const Wasm = @This();

const std = @import("std");
const builtin = @import("builtin");
const mem = std.mem;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const fs = std.fs;
const leb = std.leb;
const log = std.log.scoped(.link);

pub const Atom = @import("Wasm/Atom.zig");
const Dwarf = @import("Dwarf.zig");
const Module = @import("../Module.zig");
const InternPool = @import("../InternPool.zig");
const Compilation = @import("../Compilation.zig");
const CodeGen = @import("../arch/wasm/CodeGen.zig");
const codegen = @import("../codegen.zig");
const link = @import("../link.zig");
const lldMain = @import("../main.zig").lldMain;
const trace = @import("../tracy.zig").trace;
const build_options = @import("build_options");
const wasi_libc = @import("../wasi_libc.zig");
const Cache = std.Build.Cache;
const Type = @import("../type.zig").Type;
const TypedValue = @import("../TypedValue.zig");
const LlvmObject = @import("../codegen/llvm.zig").Object;
const Air = @import("../Air.zig");
const Liveness = @import("../Liveness.zig");
const Symbol = @import("Wasm/Symbol.zig");
const Object = @import("Wasm/Object.zig");
const Archive = @import("Wasm/Archive.zig");
const types = @import("Wasm/types.zig");
pub const Relocation = types.Relocation;

pub const base_tag: link.File.Tag = .wasm;

base: link.File,
/// Output name of the file
name: []const u8,
/// If this is not null, an object file is created by LLVM and linked with LLD afterwards.
llvm_object: ?*LlvmObject = null,
/// When importing objects from the host environment, a name must be supplied.
/// LLVM uses "env" by default when none is given. This would be a good default for Zig
/// to support existing code.
/// TODO: Allow setting this through a flag?
host_name: []const u8 = "env",
/// List of all `Decl` that are currently alive.
/// Each index maps to the corresponding `Atom.Index`.
decls: std.AutoHashMapUnmanaged(Module.Decl.Index, Atom.Index) = .{},
/// Mapping between an `Atom` and its type index representing the Wasm
/// type of the function signature.
atom_types: std.AutoHashMapUnmanaged(Atom.Index, u32) = .{},
/// List of all symbols generated by Zig code.
symbols: std.ArrayListUnmanaged(Symbol) = .{},
/// List of symbol indexes which are free to be used.
symbols_free_list: std.ArrayListUnmanaged(u32) = .{},
/// Maps atoms to their segment index
atoms: std.AutoHashMapUnmanaged(u32, Atom.Index) = .{},
/// List of all atoms.
managed_atoms: std.ArrayListUnmanaged(Atom) = .{},
/// Represents the index into `segments` where the 'code' section
/// lives.
code_section_index: ?u32 = null,
/// The index of the segment representing the custom '.debug_info' section.
debug_info_index: ?u32 = null,
/// The index of the segment representing the custom '.debug_line' section.
debug_line_index: ?u32 = null,
/// The index of the segment representing the custom '.debug_loc' section.
debug_loc_index: ?u32 = null,
/// The index of the segment representing the custom '.debug_ranges' section.
debug_ranges_index: ?u32 = null,
/// The index of the segment representing the custom '.debug_pubnames' section.
debug_pubnames_index: ?u32 = null,
/// The index of the segment representing the custom '.debug_pubtypes' section.
debug_pubtypes_index: ?u32 = null,
/// The index of the segment representing the custom '.debug_pubtypes' section.
debug_str_index: ?u32 = null,
/// The index of the segment representing the custom '.debug_pubtypes' section.
debug_abbrev_index: ?u32 = null,
/// The count of imported functions. This number will be appended
/// to the function indexes as their index starts at the lowest non-extern function.
imported_functions_count: u32 = 0,
/// The count of imported wasm globals. This number will be appended
/// to the global indexes when sections are merged.
imported_globals_count: u32 = 0,
/// The count of imported tables. This number will be appended
/// to the table indexes when sections are merged.
imported_tables_count: u32 = 0,
/// Map of symbol locations, represented by its `types.Import`
imports: std.AutoHashMapUnmanaged(SymbolLoc, types.Import) = .{},
/// Represents non-synthetic section entries.
/// Used for code, data and custom sections.
segments: std.ArrayListUnmanaged(Segment) = .{},
/// Maps a data segment key (such as .rodata) to the index into `segments`.
data_segments: std.StringArrayHashMapUnmanaged(u32) = .{},
/// A table of `types.Segment` which provide meta data
/// about a data symbol such as its name where the key is
/// the segment index, which can be found from `data_segments`
segment_info: std.AutoArrayHashMapUnmanaged(u32, types.Segment) = .{},
/// Deduplicated string table for strings used by symbols, imports and exports.
string_table: StringTable = .{},
/// Debug information for wasm
dwarf: ?Dwarf = null,

// Output sections
/// Output type section
func_types: std.ArrayListUnmanaged(std.wasm.Type) = .{},
/// Output function section where the key is the original
/// function index and the value is function.
/// This allows us to map multiple symbols to the same function.
functions: std.AutoArrayHashMapUnmanaged(struct { file: ?u16, index: u32 }, std.wasm.Func) = .{},
/// Output global section
wasm_globals: std.ArrayListUnmanaged(std.wasm.Global) = .{},
/// Memory section
memories: std.wasm.Memory = .{ .limits = .{
    .min = 0,
    .max = undefined,
    .flags = 0,
} },
/// Output table section
tables: std.ArrayListUnmanaged(std.wasm.Table) = .{},
/// Output export section
exports: std.ArrayListUnmanaged(types.Export) = .{},
/// List of initialization functions. These must be called in order of priority
/// by the (synthetic) __wasm_call_ctors function.
init_funcs: std.ArrayListUnmanaged(InitFuncLoc) = .{},
/// Index to a function defining the entry of the wasm file
entry: ?u32 = null,

/// Indirect function table, used to call function pointers
/// When this is non-zero, we must emit a table entry,
/// as well as an 'elements' section.
///
/// Note: Key is symbol location, value represents the index into the table
function_table: std.AutoHashMapUnmanaged(SymbolLoc, u32) = .{},

/// All object files and their data which are linked into the final binary
objects: std.ArrayListUnmanaged(Object) = .{},
/// All archive files that are lazy loaded.
/// e.g. when an undefined symbol references a symbol from the archive.
archives: std.ArrayListUnmanaged(Archive) = .{},

/// A map of global names (read: offset into string table) to their symbol location
globals: std.AutoHashMapUnmanaged(u32, SymbolLoc) = .{},
/// The list of GOT symbols and their location
got_symbols: std.ArrayListUnmanaged(SymbolLoc) = .{},
/// Maps discarded symbols and their positions to the location of the symbol
/// it was resolved to
discarded: std.AutoHashMapUnmanaged(SymbolLoc, SymbolLoc) = .{},
/// List of all symbol locations which have been resolved by the linker and will be emit
/// into the final binary.
resolved_symbols: std.AutoArrayHashMapUnmanaged(SymbolLoc, void) = .{},
/// Symbols that remain undefined after symbol resolution.
/// Note: The key represents an offset into the string table, rather than the actual string.
undefs: std.AutoArrayHashMapUnmanaged(u32, SymbolLoc) = .{},
/// Maps a symbol's location to an atom. This can be used to find meta
/// data of a symbol, such as its size, or its offset to perform a relocation.
/// Undefined (and synthetic) symbols do not have an Atom and therefore cannot be mapped.
symbol_atom: std.AutoHashMapUnmanaged(SymbolLoc, Atom.Index) = .{},
/// Maps a symbol's location to its export name, which may differ from the decl's name
/// which does the exporting.
/// Note: The value represents the offset into the string table, rather than the actual string.
export_names: std.AutoHashMapUnmanaged(SymbolLoc, u32) = .{},

/// Represents the symbol index of the error name table
/// When this is `null`, no code references an error using runtime `@errorName`.
/// During initializion, a symbol with corresponding atom will be created that is
/// used to perform relocations to the pointer of this table.
/// The actual table is populated during `flush`.
error_table_symbol: ?u32 = null,

// Debug section atoms. These are only set when the current compilation
// unit contains Zig code. The lifetime of these atoms are extended
// until the end of the compiler's lifetime. Meaning they're not freed
// during `flush()` in incremental-mode.
debug_info_atom: ?Atom.Index = null,
debug_line_atom: ?Atom.Index = null,
debug_loc_atom: ?Atom.Index = null,
debug_ranges_atom: ?Atom.Index = null,
debug_abbrev_atom: ?Atom.Index = null,
debug_str_atom: ?Atom.Index = null,
debug_pubnames_atom: ?Atom.Index = null,
debug_pubtypes_atom: ?Atom.Index = null,

/// List of atom indexes of functions that are generated by the backend,
/// rather than by the linker.
synthetic_functions: std.ArrayListUnmanaged(Atom.Index) = .{},

pub const Segment = struct {
    alignment: u32,
    size: u32,
    offset: u32,
    flags: u32,

    pub const Flag = enum(u32) {
        WASM_DATA_SEGMENT_IS_PASSIVE = 0x01,
        WASM_DATA_SEGMENT_HAS_MEMINDEX = 0x02,
    };

    pub fn isPassive(segment: Segment) bool {
        return segment.flags & @intFromEnum(Flag.WASM_DATA_SEGMENT_IS_PASSIVE) != 0;
    }

    /// For a given segment, determines if it needs passive initialization
    fn needsPassiveInitialization(segment: Segment, import_mem: bool, name: []const u8) bool {
        if (import_mem and !std.mem.eql(u8, name, ".bss")) {
            return true;
        }
        return segment.isPassive();
    }
};

pub const Export = struct {
    sym_index: ?u32 = null,
};

pub const SymbolLoc = struct {
    /// The index of the symbol within the specified file
    index: u32,
    /// The index of the object file where the symbol resides.
    /// When this is `null` the symbol comes from a non-object file.
    file: ?u16,

    /// From a given location, returns the corresponding symbol in the wasm binary
    pub fn getSymbol(loc: SymbolLoc, wasm_bin: *const Wasm) *Symbol {
        if (wasm_bin.discarded.get(loc)) |new_loc| {
            return new_loc.getSymbol(wasm_bin);
        }
        if (loc.file) |object_index| {
            const object = wasm_bin.objects.items[object_index];
            return &object.symtable[loc.index];
        }
        return &wasm_bin.symbols.items[loc.index];
    }

    /// From a given location, returns the name of the symbol.
    pub fn getName(loc: SymbolLoc, wasm_bin: *const Wasm) []const u8 {
        if (wasm_bin.discarded.get(loc)) |new_loc| {
            return new_loc.getName(wasm_bin);
        }
        if (loc.file) |object_index| {
            const object = wasm_bin.objects.items[object_index];
            return object.string_table.get(object.symtable[loc.index].name);
        }
        return wasm_bin.string_table.get(wasm_bin.symbols.items[loc.index].name);
    }

    /// From a given symbol location, returns the final location.
    /// e.g. when a symbol was resolved and replaced by the symbol
    /// in a different file, this will return said location.
    /// If the symbol wasn't replaced by another, this will return
    /// the given location itwasm.
    pub fn finalLoc(loc: SymbolLoc, wasm_bin: *const Wasm) SymbolLoc {
        if (wasm_bin.discarded.get(loc)) |new_loc| {
            return new_loc.finalLoc(wasm_bin);
        }
        return loc;
    }
};

// Contains the location of the function symbol, as well as
/// the priority itself of the initialization function.
pub const InitFuncLoc = struct {
    /// object file index in the list of objects.
    /// Unlike `SymbolLoc` this cannot be `null` as we never define
    /// our own ctors.
    file: u16,
    /// Symbol index within the corresponding object file.
    index: u32,
    /// The priority in which the constructor must be called.
    priority: u32,

    /// From a given `InitFuncLoc` returns the corresponding function symbol
    fn getSymbol(loc: InitFuncLoc, wasm: *const Wasm) *Symbol {
        return getSymbolLoc(loc).getSymbol(wasm);
    }

    /// Turns the given `InitFuncLoc` into a `SymbolLoc`
    fn getSymbolLoc(loc: InitFuncLoc) SymbolLoc {
        return .{ .file = loc.file, .index = loc.index };
    }

    /// Returns true when `lhs` has a higher priority (e.i. value closer to 0) than `rhs`.
    fn lessThan(ctx: void, lhs: InitFuncLoc, rhs: InitFuncLoc) bool {
        _ = ctx;
        return lhs.priority < rhs.priority;
    }
};
/// Generic string table that duplicates strings
/// and converts them into offsets instead.
pub const StringTable = struct {
    /// Table that maps string offsets, which is used to de-duplicate strings.
    /// Rather than having the offset map to the data, the `StringContext` holds all bytes of the string.
    /// The strings are stored as a contigious array where each string is zero-terminated.
    string_table: std.HashMapUnmanaged(
        u32,
        void,
        std.hash_map.StringIndexContext,
        std.hash_map.default_max_load_percentage,
    ) = .{},
    /// Holds the actual data of the string table.
    string_data: std.ArrayListUnmanaged(u8) = .{},

    /// Accepts a string and searches for a corresponding string.
    /// When found, de-duplicates the string and returns the existing offset instead.
    /// When the string is not found in the `string_table`, a new entry will be inserted
    /// and the new offset to its data will be returned.
    pub fn put(table: *StringTable, allocator: Allocator, string: []const u8) !u32 {
        const gop = try table.string_table.getOrPutContextAdapted(
            allocator,
            string,
            std.hash_map.StringIndexAdapter{ .bytes = &table.string_data },
            .{ .bytes = &table.string_data },
        );
        if (gop.found_existing) {
            const off = gop.key_ptr.*;
            log.debug("reusing string '{s}' at offset 0x{x}", .{ string, off });
            return off;
        }

        try table.string_data.ensureUnusedCapacity(allocator, string.len + 1);
        const offset = @as(u32, @intCast(table.string_data.items.len));

        log.debug("writing new string '{s}' at offset 0x{x}", .{ string, offset });

        table.string_data.appendSliceAssumeCapacity(string);
        table.string_data.appendAssumeCapacity(0);

        gop.key_ptr.* = offset;

        return offset;
    }

    /// From a given offset, returns its corresponding string value.
    /// Asserts offset does not exceed bounds.
    pub fn get(table: StringTable, off: u32) []const u8 {
        assert(off < table.string_data.items.len);
        return mem.sliceTo(@as([*:0]const u8, @ptrCast(table.string_data.items.ptr + off)), 0);
    }

    /// Returns the offset of a given string when it exists.
    /// Will return null if the given string does not yet exist within the string table.
    pub fn getOffset(table: *StringTable, string: []const u8) ?u32 {
        return table.string_table.getKeyAdapted(
            string,
            std.hash_map.StringIndexAdapter{ .bytes = &table.string_data },
        );
    }

    /// Frees all resources of the string table. Any references pointing
    /// to the strings will be invalid.
    pub fn deinit(table: *StringTable, allocator: Allocator) void {
        table.string_data.deinit(allocator);
        table.string_table.deinit(allocator);
        table.* = undefined;
    }
};

pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Options) !*Wasm {
    assert(options.target.ofmt == .wasm);

    if (options.use_llvm and options.use_lld) {
        return createEmpty(allocator, options);
    }

    const wasm_bin = try createEmpty(allocator, options);
    errdefer wasm_bin.base.destroy();

    // We are not using LLD at this point, so ensure we set the intermediary basename
    if (build_options.have_llvm and options.use_llvm and options.module != null) {
        // TODO this intermediary_basename isn't enough; in the case of `zig build-exe`,
        // we also want to put the intermediary object file in the cache while the
        // main emit directory is the cwd.
        wasm_bin.base.intermediary_basename = try std.fmt.allocPrint(allocator, "{s}{s}", .{
            options.emit.?.sub_path, options.target.ofmt.fileExt(options.target.cpu.arch),
        });
    }

    // TODO: read the file and keep valid parts instead of truncating
    const file = try options.emit.?.directory.handle.createFile(sub_path, .{
        .truncate = true,
        .read = true,
        .mode = if (fs.has_executable_bit)
            if (options.target.os.tag == .wasi and options.output_mode == .Exe)
                fs.File.default_mode | 0b001_000_000
            else
                fs.File.default_mode
        else
            0,
    });
    wasm_bin.base.file = file;
    wasm_bin.name = sub_path;

    // create stack pointer symbol
    {
        const loc = try wasm_bin.createSyntheticSymbol("__stack_pointer", .global);
        const symbol = loc.getSymbol(wasm_bin);
        // For object files we will import the stack pointer symbol
        if (options.output_mode == .Obj) {
            symbol.setUndefined(true);
            symbol.index = @as(u32, @intCast(wasm_bin.imported_globals_count));
            wasm_bin.imported_globals_count += 1;
            try wasm_bin.imports.putNoClobber(
                allocator,
                loc,
                .{
                    .module_name = try wasm_bin.string_table.put(allocator, wasm_bin.host_name),
                    .name = symbol.name,
                    .kind = .{ .global = .{ .valtype = .i32, .mutable = true } },
                },
            );
        } else {
            symbol.index = @intCast(wasm_bin.imported_globals_count + wasm_bin.wasm_globals.items.len);
            symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
            const global = try wasm_bin.wasm_globals.addOne(allocator);
            global.* = .{
                .global_type = .{
                    .valtype = .i32,
                    .mutable = true,
                },
                .init = .{ .i32_const = 0 },
            };
        }
    }

    // create indirect function pointer symbol
    {
        const loc = try wasm_bin.createSyntheticSymbol("__indirect_function_table", .table);
        const symbol = loc.getSymbol(wasm_bin);
        const table: std.wasm.Table = .{
            .limits = .{ .flags = 0, .min = 0, .max = undefined }, // will be overwritten during `mapFunctionTable`
            .reftype = .funcref,
        };
        if (options.output_mode == .Obj or options.import_table) {
            symbol.setUndefined(true);
            symbol.index = @intCast(wasm_bin.imported_tables_count);
            wasm_bin.imported_tables_count += 1;
            try wasm_bin.imports.put(allocator, loc, .{
                .module_name = try wasm_bin.string_table.put(allocator, wasm_bin.host_name),
                .name = symbol.name,
                .kind = .{ .table = table },
            });
        } else {
            symbol.index = @as(u32, @intCast(wasm_bin.imported_tables_count + wasm_bin.tables.items.len));
            try wasm_bin.tables.append(allocator, table);
            if (options.export_table) {
                symbol.setFlag(.WASM_SYM_EXPORTED);
            } else {
                symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
            }
        }
    }

    // create __wasm_call_ctors
    {
        const loc = try wasm_bin.createSyntheticSymbol("__wasm_call_ctors", .function);
        const symbol = loc.getSymbol(wasm_bin);
        symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
        // we do not know the function index until after we merged all sections.
        // Therefore we set `symbol.index` and create its corresponding references
        // at the end during `initializeCallCtorsFunction`.
    }

    // shared-memory symbols for TLS support
    if (wasm_bin.base.options.shared_memory) {
        {
            const loc = try wasm_bin.createSyntheticSymbol("__tls_base", .global);
            const symbol = loc.getSymbol(wasm_bin);
            symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
            symbol.index = @intCast(wasm_bin.imported_globals_count + wasm_bin.wasm_globals.items.len);
            try wasm_bin.wasm_globals.append(wasm_bin.base.allocator, .{
                .global_type = .{ .valtype = .i32, .mutable = true },
                .init = .{ .i32_const = undefined },
            });
        }
        {
            const loc = try wasm_bin.createSyntheticSymbol("__tls_size", .global);
            const symbol = loc.getSymbol(wasm_bin);
            symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
            symbol.index = @intCast(wasm_bin.imported_globals_count + wasm_bin.wasm_globals.items.len);
            try wasm_bin.wasm_globals.append(wasm_bin.base.allocator, .{
                .global_type = .{ .valtype = .i32, .mutable = false },
                .init = .{ .i32_const = undefined },
            });
        }
        {
            const loc = try wasm_bin.createSyntheticSymbol("__tls_align", .global);
            const symbol = loc.getSymbol(wasm_bin);
            symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
            symbol.index = @intCast(wasm_bin.imported_globals_count + wasm_bin.wasm_globals.items.len);
            try wasm_bin.wasm_globals.append(wasm_bin.base.allocator, .{
                .global_type = .{ .valtype = .i32, .mutable = false },
                .init = .{ .i32_const = undefined },
            });
        }
        {
            const loc = try wasm_bin.createSyntheticSymbol("__wasm_init_tls", .function);
            const symbol = loc.getSymbol(wasm_bin);
            symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
        }
    }

    // if (!options.strip and options.module != null) {
    //     wasm_bin.dwarf = Dwarf.init(allocator, &wasm_bin.base, options.target);
    //     try wasm_bin.initDebugSections();
    // }

    return wasm_bin;
}

pub fn createEmpty(gpa: Allocator, options: link.Options) !*Wasm {
    const wasm = try gpa.create(Wasm);
    errdefer gpa.destroy(wasm);
    wasm.* = .{
        .base = .{
            .tag = .wasm,
            .options = options,
            .file = null,
            .allocator = gpa,
        },
        .name = undefined,
    };

    if (options.use_llvm) {
        wasm.llvm_object = try LlvmObject.create(gpa, options);
    }
    return wasm;
}

/// For a given name, creates a new global synthetic symbol.
/// Leaves index undefined and the default flags (0).
fn createSyntheticSymbol(wasm: *Wasm, name: []const u8, tag: Symbol.Tag) !SymbolLoc {
    const name_offset = try wasm.string_table.put(wasm.base.allocator, name);
    return wasm.createSyntheticSymbolOffset(name_offset, tag);
}

fn createSyntheticSymbolOffset(wasm: *Wasm, name_offset: u32, tag: Symbol.Tag) !SymbolLoc {
    const sym_index = @as(u32, @intCast(wasm.symbols.items.len));
    const loc: SymbolLoc = .{ .index = sym_index, .file = null };
    try wasm.symbols.append(wasm.base.allocator, .{
        .name = name_offset,
        .flags = 0,
        .tag = tag,
        .index = undefined,
        .virtual_address = undefined,
    });
    try wasm.resolved_symbols.putNoClobber(wasm.base.allocator, loc, {});
    try wasm.globals.put(wasm.base.allocator, name_offset, loc);
    return loc;
}

/// Initializes symbols and atoms for the debug sections
/// Initialization is only done when compiling Zig code.
/// When Zig is invoked as a linker instead, the atoms
/// and symbols come from the object files instead.
pub fn initDebugSections(wasm: *Wasm) !void {
    if (wasm.dwarf == null) return; // not compiling Zig code, so no need to pre-initialize debug sections
    assert(wasm.debug_info_index == null);
    // this will create an Atom and set the index for us.
    wasm.debug_info_atom = try wasm.createDebugSectionForIndex(&wasm.debug_info_index, ".debug_info");
    wasm.debug_line_atom = try wasm.createDebugSectionForIndex(&wasm.debug_line_index, ".debug_line");
    wasm.debug_loc_atom = try wasm.createDebugSectionForIndex(&wasm.debug_loc_index, ".debug_loc");
    wasm.debug_abbrev_atom = try wasm.createDebugSectionForIndex(&wasm.debug_abbrev_index, ".debug_abbrev");
    wasm.debug_ranges_atom = try wasm.createDebugSectionForIndex(&wasm.debug_ranges_index, ".debug_ranges");
    wasm.debug_str_atom = try wasm.createDebugSectionForIndex(&wasm.debug_str_index, ".debug_str");
    wasm.debug_pubnames_atom = try wasm.createDebugSectionForIndex(&wasm.debug_pubnames_index, ".debug_pubnames");
    wasm.debug_pubtypes_atom = try wasm.createDebugSectionForIndex(&wasm.debug_pubtypes_index, ".debug_pubtypes");
}

fn parseInputFiles(wasm: *Wasm, files: []const []const u8) !void {
    for (files) |path| {
        if (try wasm.parseObjectFile(path)) continue;
        if (try wasm.parseArchive(path, false)) continue; // load archives lazily
        log.warn("Unexpected file format at path: '{s}'", .{path});
    }
}

/// Parses the object file from given path. Returns true when the given file was an object
/// file and parsed successfully. Returns false when file is not an object file.
/// May return an error instead when parsing failed.
fn parseObjectFile(wasm: *Wasm, path: []const u8) !bool {
    const file = try fs.cwd().openFile(path, .{});
    errdefer file.close();

    var object = Object.create(wasm.base.allocator, file, path, null) catch |err| switch (err) {
        error.InvalidMagicByte, error.NotObjectFile => return false,
        else => |e| return e,
    };
    errdefer object.deinit(wasm.base.allocator);
    try wasm.objects.append(wasm.base.allocator, object);
    return true;
}

/// For a given `Module.Decl.Index` returns its corresponding `Atom.Index`.
/// When the index was not found, a new `Atom` will be created, and its index will be returned.
/// The newly created Atom is empty with default fields as specified by `Atom.empty`.
pub fn getOrCreateAtomForDecl(wasm: *Wasm, decl_index: Module.Decl.Index) !Atom.Index {
    const gop = try wasm.decls.getOrPut(wasm.base.allocator, decl_index);
    if (!gop.found_existing) {
        gop.value_ptr.* = try wasm.createAtom();
    }
    return gop.value_ptr.*;
}

/// Creates a new empty `Atom` and returns its `Atom.Index`
fn createAtom(wasm: *Wasm) !Atom.Index {
    const index = @as(Atom.Index, @intCast(wasm.managed_atoms.items.len));
    const atom = try wasm.managed_atoms.addOne(wasm.base.allocator);
    atom.* = Atom.empty;
    atom.sym_index = try wasm.allocateSymbol();
    try wasm.symbol_atom.putNoClobber(wasm.base.allocator, .{ .file = null, .index = atom.sym_index }, index);

    return index;
}

pub inline fn getAtom(wasm: *const Wasm, index: Atom.Index) Atom {
    return wasm.managed_atoms.items[index];
}

pub inline fn getAtomPtr(wasm: *Wasm, index: Atom.Index) *Atom {
    return &wasm.managed_atoms.items[index];
}

/// Parses an archive file and will then parse each object file
/// that was found in the archive file.
/// Returns false when the file is not an archive file.
/// May return an error instead when parsing failed.
///
/// When `force_load` is `true`, it will for link all object files in the archive.
/// When false, it will only link with object files that contain symbols that
/// are referenced by other object files or Zig code.
fn parseArchive(wasm: *Wasm, path: []const u8, force_load: bool) !bool {
    const file = try fs.cwd().openFile(path, .{});
    errdefer file.close();

    var archive: Archive = .{
        .file = file,
        .name = path,
    };
    archive.parse(wasm.base.allocator) catch |err| switch (err) {
        error.EndOfStream, error.NotArchive => {
            archive.deinit(wasm.base.allocator);
            return false;
        },
        else => |e| return e,
    };

    if (!force_load) {
        errdefer archive.deinit(wasm.base.allocator);
        try wasm.archives.append(wasm.base.allocator, archive);
        return true;
    }
    defer archive.deinit(wasm.base.allocator);

    // In this case we must force link all embedded object files within the archive
    // We loop over all symbols, and then group them by offset as the offset
    // notates where the object file starts.
    var offsets = std.AutoArrayHashMap(u32, void).init(wasm.base.allocator);
    defer offsets.deinit();
    for (archive.toc.values()) |symbol_offsets| {
        for (symbol_offsets.items) |sym_offset| {
            try offsets.put(sym_offset, {});
        }
    }

    for (offsets.keys()) |file_offset| {
        const object = try wasm.objects.addOne(wasm.base.allocator);
        object.* = try archive.parseObject(wasm.base.allocator, file_offset);
    }

    return true;
}

fn requiresTLSReloc(wasm: *const Wasm) bool {
    for (wasm.got_symbols.items) |loc| {
        if (loc.getSymbol(wasm).isTLS()) {
            return true;
        }
    }
    return false;
}

fn resolveSymbolsInObject(wasm: *Wasm, object_index: u16) !void {
    const object: Object = wasm.objects.items[object_index];
    log.debug("Resolving symbols in object: '{s}'", .{object.name});

    for (object.symtable, 0..) |symbol, i| {
        const sym_index = @as(u32, @intCast(i));
        const location: SymbolLoc = .{
            .file = object_index,
            .index = sym_index,
        };
        const sym_name = object.string_table.get(symbol.name);
        if (mem.eql(u8, sym_name, "__indirect_function_table")) {
            continue;
        }
        const sym_name_index = try wasm.string_table.put(wasm.base.allocator, sym_name);

        if (symbol.isLocal()) {
            if (symbol.isUndefined()) {
                log.err("Local symbols are not allowed to reference imports", .{});
                log.err("  symbol '{s}' defined in '{s}'", .{ sym_name, object.name });
                return error.UndefinedLocal;
            }
            try wasm.resolved_symbols.putNoClobber(wasm.base.allocator, location, {});
            continue;
        }

        const maybe_existing = try wasm.globals.getOrPut(wasm.base.allocator, sym_name_index);
        if (!maybe_existing.found_existing) {
            maybe_existing.value_ptr.* = location;
            try wasm.resolved_symbols.putNoClobber(wasm.base.allocator, location, {});

            if (symbol.isUndefined()) {
                try wasm.undefs.putNoClobber(wasm.base.allocator, sym_name_index, location);
            }
            continue;
        }

        const existing_loc = maybe_existing.value_ptr.*;
        const existing_sym: *Symbol = existing_loc.getSymbol(wasm);

        const existing_file_path = if (existing_loc.file) |file| blk: {
            break :blk wasm.objects.items[file].name;
        } else wasm.name;

        if (!existing_sym.isUndefined()) outer: {
            if (!symbol.isUndefined()) inner: {
                if (symbol.isWeak()) {
                    break :inner; // ignore the new symbol (discard it)
                }
                if (existing_sym.isWeak()) {
                    break :outer; // existing is weak, while new one isn't. Replace it.
                }
                // both are defined and weak, we have a symbol collision.
                log.err("symbol '{s}' defined multiple times", .{sym_name});
                log.err("  first definition in '{s}'", .{existing_file_path});
                log.err("  next definition in '{s}'", .{object.name});
                return error.SymbolCollision;
            }

            try wasm.discarded.put(wasm.base.allocator, location, existing_loc);
            continue; // Do not overwrite defined symbols with undefined symbols
        }

        if (symbol.tag != existing_sym.tag) {
            log.err("symbol '{s}' mismatching type '{s}", .{ sym_name, @tagName(symbol.tag) });
            log.err("  first definition in '{s}'", .{existing_file_path});
            log.err("  next definition in '{s}'", .{object.name});
            return error.SymbolMismatchingType;
        }

        if (existing_sym.isUndefined() and symbol.isUndefined()) {
            // only verify module/import name for function symbols
            if (symbol.tag == .function) {
                const existing_name = if (existing_loc.file) |file_index| blk: {
                    const obj = wasm.objects.items[file_index];
                    const name_index = obj.findImport(symbol.tag.externalType(), existing_sym.index).module_name;
                    break :blk obj.string_table.get(name_index);
                } else blk: {
                    const name_index = wasm.imports.get(existing_loc).?.module_name;
                    break :blk wasm.string_table.get(name_index);
                };

                const module_index = object.findImport(symbol.tag.externalType(), symbol.index).module_name;
                const module_name = object.string_table.get(module_index);
                if (!mem.eql(u8, existing_name, module_name)) {
                    log.err("symbol '{s}' module name mismatch. Expected '{s}', but found '{s}'", .{
                        sym_name,
                        existing_name,
                        module_name,
                    });
                    log.err("  first definition in '{s}'", .{existing_file_path});
                    log.err("  next definition in '{s}'", .{object.name});
                    return error.ModuleNameMismatch;
                }
            }

            // both undefined so skip overwriting existing symbol and discard the new symbol
            try wasm.discarded.put(wasm.base.allocator, location, existing_loc);
            continue;
        }

        if (existing_sym.tag == .global) {
            const existing_ty = wasm.getGlobalType(existing_loc);
            const new_ty = wasm.getGlobalType(location);
            if (existing_ty.mutable != new_ty.mutable or existing_ty.valtype != new_ty.valtype) {
                log.err("symbol '{s}' mismatching global types", .{sym_name});
                log.err("  first definition in '{s}'", .{existing_file_path});
                log.err("  next definition in '{s}'", .{object.name});
                return error.GlobalTypeMismatch;
            }
        }

        if (existing_sym.tag == .function) {
            const existing_ty = wasm.getFunctionSignature(existing_loc);
            const new_ty = wasm.getFunctionSignature(location);
            if (!existing_ty.eql(new_ty)) {
                log.err("symbol '{s}' mismatching function signatures.", .{sym_name});
                log.err("  expected signature {}, but found signature {}", .{ existing_ty, new_ty });
                log.err("  first definition in '{s}'", .{existing_file_path});
                log.err("  next definition in '{s}'", .{object.name});
                return error.FunctionSignatureMismatch;
            }
        }

        // when both symbols are weak, we skip overwriting unless the existing
        // symbol is weak and the new one isn't, in which case we *do* overwrite it.
        if (existing_sym.isWeak() and symbol.isWeak()) blk: {
            if (existing_sym.isUndefined() and !symbol.isUndefined()) break :blk;
            try wasm.discarded.put(wasm.base.allocator, location, existing_loc);
            continue;
        }

        // simply overwrite with the new symbol
        log.debug("Overwriting symbol '{s}'", .{sym_name});
        log.debug("  old definition in '{s}'", .{existing_file_path});
        log.debug("  new definition in '{s}'", .{object.name});
        try wasm.discarded.putNoClobber(wasm.base.allocator, existing_loc, location);
        maybe_existing.value_ptr.* = location;
        try wasm.globals.put(wasm.base.allocator, sym_name_index, location);
        try wasm.resolved_symbols.put(wasm.base.allocator, location, {});
        assert(wasm.resolved_symbols.swapRemove(existing_loc));
        if (existing_sym.isUndefined()) {
            _ = wasm.undefs.swapRemove(sym_name_index);
        }
    }
}

fn resolveSymbolsInArchives(wasm: *Wasm) !void {
    if (wasm.archives.items.len == 0) return;

    log.debug("Resolving symbols in archives", .{});
    var index: u32 = 0;
    undef_loop: while (index < wasm.undefs.count()) {
        const sym_name_index = wasm.undefs.keys()[index];

        for (wasm.archives.items) |archive| {
            const sym_name = wasm.string_table.get(sym_name_index);
            log.debug("Detected symbol '{s}' in archive '{s}', parsing objects..", .{ sym_name, archive.name });
            const offset = archive.toc.get(sym_name) orelse {
                // symbol does not exist in this archive
                continue;
            };

            // Symbol is found in unparsed object file within current archive.
            // Parse object and and resolve symbols again before we check remaining
            // undefined symbols.
            const object_file_index = @as(u16, @intCast(wasm.objects.items.len));
            var object = try archive.parseObject(wasm.base.allocator, offset.items[0]);
            try wasm.objects.append(wasm.base.allocator, object);
            try wasm.resolveSymbolsInObject(object_file_index);

            // continue loop for any remaining undefined symbols that still exist
            // after resolving last object file
            continue :undef_loop;
        }
        index += 1;
    }
}

/// Writes an unsigned 32-bit integer as a LEB128-encoded 'i32.const' value.
fn writeI32Const(writer: anytype, val: u32) !void {
    try writer.writeByte(std.wasm.opcode(.i32_const));
    try leb.writeILEB128(writer, @as(i32, @bitCast(val)));
}

fn setupInitMemoryFunction(wasm: *Wasm) !void {
    // Passive segments are used to avoid memory being reinitialized on each
    // thread's instantiation. These passive segments are initialized and
    // dropped in __wasm_init_memory, which is registered as the start function
    // We also initialize bss segments (using memory.fill) as part of this
    // function.
    if (!wasm.hasPassiveInitializationSegments()) {
        return;
    }

    const flag_address: u32 = if (wasm.base.options.shared_memory) address: {
        // when we have passive initialization segments and shared memory
        // `setupMemory` will create this symbol and set its virtual address.
        const loc = wasm.findGlobalSymbol("__wasm_init_memory_flag").?;
        break :address loc.getSymbol(wasm).virtual_address;
    } else 0;

    var function_body = std.ArrayList(u8).init(wasm.base.allocator);
    defer function_body.deinit();
    const writer = function_body.writer();

    // we have 0 locals
    try leb.writeULEB128(writer, @as(u32, 0));

    if (wasm.base.options.shared_memory) {
        // destination blocks
        // based on values we jump to corresponding label
        try writer.writeByte(std.wasm.opcode(.block)); // $drop
        try writer.writeByte(std.wasm.block_empty); // block type

        try writer.writeByte(std.wasm.opcode(.block)); // $wait
        try writer.writeByte(std.wasm.block_empty); // block type

        try writer.writeByte(std.wasm.opcode(.block)); // $init
        try writer.writeByte(std.wasm.block_empty); // block type

        // atomically check
        try writeI32Const(writer, flag_address);
        try writeI32Const(writer, 0);
        try writeI32Const(writer, 1);
        try writer.writeByte(std.wasm.opcode(.atomics_prefix));
        try leb.writeULEB128(writer, std.wasm.atomicsOpcode(.i32_atomic_rmw_cmpxchg));
        try leb.writeULEB128(writer, @as(u32, 2)); // alignment
        try leb.writeULEB128(writer, @as(u32, 0)); // offset

        // based on the value from the atomic check, jump to the label.
        try writer.writeByte(std.wasm.opcode(.br_table));
        try leb.writeULEB128(writer, @as(u32, 2)); // length of the table (we have 3 blocks but because of the mandatory default the length is 2).
        try leb.writeULEB128(writer, @as(u32, 0)); // $init
        try leb.writeULEB128(writer, @as(u32, 1)); // $wait
        try leb.writeULEB128(writer, @as(u32, 2)); // $drop
        try writer.writeByte(std.wasm.opcode(.end));
    }

    var it = wasm.data_segments.iterator();
    var segment_index: u32 = 0;
    while (it.next()) |entry| : (segment_index += 1) {
        const segment: Segment = wasm.segments.items[entry.value_ptr.*];
        if (segment.needsPassiveInitialization(wasm.base.options.import_memory, entry.key_ptr.*)) {
            // For passive BSS segments we can simple issue a memory.fill(0).
            // For non-BSS segments we do a memory.init.  Both these
            // instructions take as their first argument the destination
            // address.
            try writeI32Const(writer, segment.offset);

            if (wasm.base.options.shared_memory and std.mem.eql(u8, entry.key_ptr.*, ".tdata")) {
                // When we initialize the TLS segment we also set the `__tls_base`
                // global.  This allows the runtime to use this static copy of the
                // TLS data for the first/main thread.
                try writeI32Const(writer, segment.offset);
                try writer.writeByte(std.wasm.opcode(.global_set));
                const loc = wasm.findGlobalSymbol("__tls_base").?;
                try leb.writeULEB128(writer, loc.getSymbol(wasm).index);
            }

            try writeI32Const(writer, 0);
            try writeI32Const(writer, segment.size);
            try writer.writeByte(std.wasm.opcode(.misc_prefix));
            if (std.mem.eql(u8, entry.key_ptr.*, ".bss")) {
                // fill bss segment with zeroes
                try leb.writeULEB128(writer, std.wasm.miscOpcode(.memory_fill));
            } else {
                // initialize the segment
                try leb.writeULEB128(writer, std.wasm.miscOpcode(.memory_init));
                try leb.writeULEB128(writer, segment_index);
            }
            try writer.writeByte(0); // memory index immediate
        }
    }

    if (wasm.base.options.shared_memory) {
        // we set the init memory flag to value '2'
        try writeI32Const(writer, flag_address);
        try writeI32Const(writer, 2);
        try writer.writeByte(std.wasm.opcode(.atomics_prefix));
        try leb.writeULEB128(writer, std.wasm.atomicsOpcode(.i32_atomic_store));
        try leb.writeULEB128(writer, @as(u32, 2)); // alignment
        try leb.writeULEB128(writer, @as(u32, 0)); // offset

        // notify any waiters for segment initialization completion
        try writeI32Const(writer, flag_address);
        try writer.writeByte(std.wasm.opcode(.i32_const));
        try leb.writeILEB128(writer, @as(i32, -1)); // number of waiters
        try writer.writeByte(std.wasm.opcode(.atomics_prefix));
        try leb.writeULEB128(writer, std.wasm.atomicsOpcode(.memory_atomic_notify));
        try leb.writeULEB128(writer, @as(u32, 2)); // alignment
        try leb.writeULEB128(writer, @as(u32, 0)); // offset
        try writer.writeByte(std.wasm.opcode(.drop));

        // branch and drop segments
        try writer.writeByte(std.wasm.opcode(.br));
        try leb.writeULEB128(writer, @as(u32, 1));

        // wait for thread to initialize memory segments
        try writer.writeByte(std.wasm.opcode(.end)); // end $wait
        try writeI32Const(writer, flag_address);
        try writeI32Const(writer, 1); // expected flag value
        try writer.writeByte(std.wasm.opcode(.i64_const));
        try leb.writeILEB128(writer, @as(i64, -1)); // timeout
        try writer.writeByte(std.wasm.opcode(.atomics_prefix));
        try leb.writeULEB128(writer, std.wasm.atomicsOpcode(.memory_atomic_wait32));
        try leb.writeULEB128(writer, @as(u32, 2)); // alignment
        try leb.writeULEB128(writer, @as(u32, 0)); // offset
        try writer.writeByte(std.wasm.opcode(.drop));

        try writer.writeByte(std.wasm.opcode(.end)); // end $drop
    }

    it.reset();
    segment_index = 0;
    while (it.next()) |entry| : (segment_index += 1) {
        const name = entry.key_ptr.*;
        const segment: Segment = wasm.segments.items[entry.value_ptr.*];
        if (segment.needsPassiveInitialization(wasm.base.options.import_memory, name) and
            !std.mem.eql(u8, name, ".bss"))
        {
            // The TLS region should not be dropped since its is needed
            // during the initialization of each thread (__wasm_init_tls).
            if (wasm.base.options.shared_memory and std.mem.eql(u8, name, ".tdata")) {
                continue;
            }

            try writer.writeByte(std.wasm.opcode(.misc_prefix));
            try leb.writeULEB128(writer, std.wasm.miscOpcode(.data_drop));
            try leb.writeULEB128(writer, segment_index);
        }
    }

    // End of the function body
    try writer.writeByte(std.wasm.opcode(.end));

    try wasm.createSyntheticFunction(
        "__wasm_init_memory",
        std.wasm.Type{ .params = &.{}, .returns = &.{} },
        &function_body,
    );
}

/// Constructs a synthetic function that performs runtime relocations for
/// TLS symbols. This function is called by `__wasm_init_tls`.
fn setupTLSRelocationsFunction(wasm: *Wasm) !void {
    // When we have TLS GOT entries and shared memory is enabled,
    // we must perform runtime relocations or else we don't create the function.
    if (!wasm.base.options.shared_memory or !wasm.requiresTLSReloc()) {
        return;
    }

    // const loc = try wasm.createSyntheticSymbol("__wasm_apply_global_tls_relocs");
    var function_body = std.ArrayList(u8).init(wasm.base.allocator);
    defer function_body.deinit();
    const writer = function_body.writer();

    // locals (we have none)
    try writer.writeByte(0);
    for (wasm.got_symbols.items, 0..) |got_loc, got_index| {
        const sym: *Symbol = got_loc.getSymbol(wasm);
        if (!sym.isTLS()) continue; // only relocate TLS symbols
        if (sym.tag == .data and sym.isDefined()) {
            // get __tls_base
            try writer.writeByte(std.wasm.opcode(.global_get));
            try leb.writeULEB128(writer, wasm.findGlobalSymbol("__tls_base").?.getSymbol(wasm).index);

            // add the virtual address of the symbol
            try writer.writeByte(std.wasm.opcode(.i32_const));
            try leb.writeULEB128(writer, sym.virtual_address);
        } else if (sym.tag == .function) {
            @panic("TODO: relocate GOT entry of function");
        } else continue;

        try writer.writeByte(std.wasm.opcode(.i32_add));
        try writer.writeByte(std.wasm.opcode(.global_set));
        try leb.writeULEB128(writer, wasm.imported_globals_count + @as(u32, @intCast(wasm.wasm_globals.items.len + got_index)));
    }
    try writer.writeByte(std.wasm.opcode(.end));

    try wasm.createSyntheticFunction(
        "__wasm_apply_global_tls_relocs",
        std.wasm.Type{ .params = &.{}, .returns = &.{} },
        &function_body,
    );
}

fn validateFeatures(
    wasm: *const Wasm,
    to_emit: *[@typeInfo(types.Feature.Tag).Enum.fields.len]bool,
    emit_features_count: *u32,
) !void {
    const cpu_features = wasm.base.options.target.cpu.features;
    const infer = cpu_features.isEmpty(); // when the user did not define any features, we infer them from linked objects.
    const known_features_count = @typeInfo(types.Feature.Tag).Enum.fields.len;

    var allowed = [_]bool{false} ** known_features_count;
    var used = [_]u17{0} ** known_features_count;
    var disallowed = [_]u17{0} ** known_features_count;
    var required = [_]u17{0} ** known_features_count;

    // when false, we fail linking. We only verify this after a loop to catch all invalid features.
    var valid_feature_set = true;
    // will be set to true when there's any TLS segment found in any of the object files
    var has_tls = false;

    // When the user has given an explicit list of features to enable,
    // we extract them and insert each into the 'allowed' list.
    if (!infer) {
        inline for (@typeInfo(std.Target.wasm.Feature).Enum.fields) |feature_field| {
            if (cpu_features.isEnabled(feature_field.value)) {
                allowed[feature_field.value] = true;
                emit_features_count.* += 1;
            }
        }
    }

    // extract all the used, disallowed and required features from each
    // linked object file so we can test them.
    for (wasm.objects.items, 0..) |object, object_index| {
        for (object.features) |feature| {
            const value = @as(u16, @intCast(object_index)) << 1 | @as(u1, 1);
            switch (feature.prefix) {
                .used => {
                    used[@intFromEnum(feature.tag)] = value;
                },
                .disallowed => {
                    disallowed[@intFromEnum(feature.tag)] = value;
                },
                .required => {
                    required[@intFromEnum(feature.tag)] = value;
                    used[@intFromEnum(feature.tag)] = value;
                },
            }
        }

        for (object.segment_info) |segment| {
            if (segment.isTLS()) {
                has_tls = true;
            }
        }
    }

    // when we infer the features, we allow each feature found in the 'used' set
    // and insert it into the 'allowed' set. When features are not inferred,
    // we validate that a used feature is allowed.
    for (used, 0..) |used_set, used_index| {
        const is_enabled = @as(u1, @truncate(used_set)) != 0;
        if (infer) {
            allowed[used_index] = is_enabled;
            emit_features_count.* += @intFromBool(is_enabled);
        } else if (is_enabled and !allowed[used_index]) {
            log.err("feature '{}' not allowed, but used by linked object", .{@as(types.Feature.Tag, @enumFromInt(used_index))});
            log.err("  defined in '{s}'", .{wasm.objects.items[used_set >> 1].name});
            valid_feature_set = false;
        }
    }

    if (!valid_feature_set) {
        return error.InvalidFeatureSet;
    }

    if (wasm.base.options.shared_memory) {
        const disallowed_feature = disallowed[@intFromEnum(types.Feature.Tag.shared_mem)];
        if (@as(u1, @truncate(disallowed_feature)) != 0) {
            log.err(
                "shared-memory is disallowed by '{s}' because it wasn't compiled with 'atomics' and 'bulk-memory' features enabled",
                .{wasm.objects.items[disallowed_feature >> 1].name},
            );
            valid_feature_set = false;
        }

        for ([_]types.Feature.Tag{ .atomics, .bulk_memory }) |feature| {
            if (!allowed[@intFromEnum(feature)]) {
                log.err("feature '{}' is not used but is required for shared-memory", .{feature});
            }
        }
    }

    if (has_tls) {
        for ([_]types.Feature.Tag{ .atomics, .bulk_memory }) |feature| {
            if (!allowed[@intFromEnum(feature)]) {
                log.err("feature '{}' is not used but is required for thread-local storage", .{feature});
            }
        }
    }
    // For each linked object, validate the required and disallowed features
    for (wasm.objects.items) |object| {
        var object_used_features = [_]bool{false} ** known_features_count;
        for (object.features) |feature| {
            if (feature.prefix == .disallowed) continue; // already defined in 'disallowed' set.
            // from here a feature is always used
            const disallowed_feature = disallowed[@intFromEnum(feature.tag)];
            if (@as(u1, @truncate(disallowed_feature)) != 0) {
                log.err("feature '{}' is disallowed, but used by linked object", .{feature.tag});
                log.err("  disallowed by '{s}'", .{wasm.objects.items[disallowed_feature >> 1].name});
                log.err("  used in '{s}'", .{object.name});
                valid_feature_set = false;
            }

            object_used_features[@intFromEnum(feature.tag)] = true;
        }

        // validate the linked object file has each required feature
        for (required, 0..) |required_feature, feature_index| {
            const is_required = @as(u1, @truncate(required_feature)) != 0;
            if (is_required and !object_used_features[feature_index]) {
                log.err("feature '{}' is required but not used in linked object", .{@as(types.Feature.Tag, @enumFromInt(feature_index))});
                log.err("  required by '{s}'", .{wasm.objects.items[required_feature >> 1].name});
                log.err("  missing in '{s}'", .{object.name});
                valid_feature_set = false;
            }
        }
    }

    if (!valid_feature_set) {
        return error.InvalidFeatureSet;
    }

    to_emit.* = allowed;
}

/// Creates synthetic linker-symbols, but only if they are being referenced from
/// any object file. For instance, the `__heap_base` symbol will only be created,
/// if one or multiple undefined references exist. When none exist, the symbol will
/// not be created, ensuring we don't unneccesarily emit unreferenced symbols.
fn resolveLazySymbols(wasm: *Wasm) !void {
    if (wasm.string_table.getOffset("__heap_base")) |name_offset| {
        if (wasm.undefs.fetchSwapRemove(name_offset)) |kv| {
            const loc = try wasm.createSyntheticSymbolOffset(name_offset, .data);
            try wasm.discarded.putNoClobber(wasm.base.allocator, kv.value, loc);
            _ = wasm.resolved_symbols.swapRemove(loc); // we don't want to emit this symbol, only use it for relocations.
        }
    }

    if (wasm.string_table.getOffset("__heap_end")) |name_offset| {
        if (wasm.undefs.fetchSwapRemove(name_offset)) |kv| {
            const loc = try wasm.createSyntheticSymbolOffset(name_offset, .data);
            try wasm.discarded.putNoClobber(wasm.base.allocator, kv.value, loc);
            _ = wasm.resolved_symbols.swapRemove(loc);
        }
    }

    if (!wasm.base.options.shared_memory) {
        if (wasm.string_table.getOffset("__tls_base")) |name_offset| {
            if (wasm.undefs.fetchSwapRemove(name_offset)) |kv| {
                const loc = try wasm.createSyntheticSymbolOffset(name_offset, .global);
                try wasm.discarded.putNoClobber(wasm.base.allocator, kv.value, loc);
            }
        }
    }
    if (wasm.string_table.getOffset("__zig_errors_len")) |name_offset| {
        if (wasm.undefs.fetchSwapRemove(name_offset)) |kv| {
            const loc = try wasm.createSyntheticSymbolOffset(name_offset, .data);
            try wasm.discarded.putNoClobber(wasm.base.allocator, kv.value, loc);
            _ = wasm.resolved_symbols.swapRemove(kv.value);
        }
    }
}

// Tries to find a global symbol by its name. Returns null when not found,
/// and its location when it is found.
pub fn findGlobalSymbol(wasm: *Wasm, name: []const u8) ?SymbolLoc {
    const offset = wasm.string_table.getOffset(name) orelse return null;
    return wasm.globals.get(offset);
}

fn checkUndefinedSymbols(wasm: *const Wasm) !void {
    if (wasm.base.options.output_mode == .Obj) return;
    if (wasm.base.options.import_symbols) return;

    var found_undefined_symbols = false;
    for (wasm.undefs.values()) |undef| {
        const symbol = undef.getSymbol(wasm);
        if (symbol.tag == .data) {
            found_undefined_symbols = true;
            const file_name = if (undef.file) |file_index| name: {
                break :name wasm.objects.items[file_index].name;
            } else wasm.name;
            const symbol_name = undef.getName(wasm);
            log.err("could not resolve undefined symbol '{s}'", .{symbol_name});
            log.err("  defined in '{s}'", .{file_name});
        }
    }
    if (found_undefined_symbols) {
        return error.UndefinedSymbol;
    }
}

pub fn deinit(wasm: *Wasm) void {
    const gpa = wasm.base.allocator;
    if (wasm.llvm_object) |llvm_object| llvm_object.destroy(gpa);

    for (wasm.func_types.items) |*func_type| {
        func_type.deinit(gpa);
    }
    for (wasm.segment_info.values()) |segment_info| {
        gpa.free(segment_info.name);
    }
    for (wasm.objects.items) |*object| {
        object.deinit(gpa);
    }

    for (wasm.archives.items) |*archive| {
        archive.deinit(gpa);
    }

    wasm.decls.deinit(gpa);
    wasm.atom_types.deinit(gpa);
    wasm.symbols.deinit(gpa);
    wasm.symbols_free_list.deinit(gpa);
    wasm.globals.deinit(gpa);
    wasm.resolved_symbols.deinit(gpa);
    wasm.undefs.deinit(gpa);
    wasm.discarded.deinit(gpa);
    wasm.symbol_atom.deinit(gpa);
    wasm.export_names.deinit(gpa);
    wasm.atoms.deinit(gpa);
    for (wasm.managed_atoms.items) |*managed_atom| {
        managed_atom.deinit(wasm);
    }
    wasm.managed_atoms.deinit(gpa);
    wasm.segments.deinit(gpa);
    wasm.data_segments.deinit(gpa);
    wasm.segment_info.deinit(gpa);
    wasm.objects.deinit(gpa);
    wasm.archives.deinit(gpa);

    // free output sections
    wasm.imports.deinit(gpa);
    wasm.func_types.deinit(gpa);
    wasm.functions.deinit(gpa);
    wasm.wasm_globals.deinit(gpa);
    wasm.function_table.deinit(gpa);
    wasm.tables.deinit(gpa);
    wasm.init_funcs.deinit(gpa);
    wasm.exports.deinit(gpa);

    wasm.string_table.deinit(gpa);
    wasm.synthetic_functions.deinit(gpa);

    if (wasm.dwarf) |*dwarf| {
        dwarf.deinit();
    }
}

/// Allocates a new symbol and returns its index.
/// Will re-use slots when a symbol was freed at an earlier stage.
pub fn allocateSymbol(wasm: *Wasm) !u32 {
    try wasm.symbols.ensureUnusedCapacity(wasm.base.allocator, 1);
    var symbol: Symbol = .{
        .name = undefined, // will be set after updateDecl
        .flags = @intFromEnum(Symbol.Flag.WASM_SYM_BINDING_LOCAL),
        .tag = undefined, // will be set after updateDecl
        .index = undefined, // will be set after updateDecl
        .virtual_address = undefined, // will be set during atom allocation
    };
    if (wasm.symbols_free_list.popOrNull()) |index| {
        wasm.symbols.items[index] = symbol;
        return index;
    }
    const index = @as(u32, @intCast(wasm.symbols.items.len));
    wasm.symbols.appendAssumeCapacity(symbol);
    return index;
}

pub fn updateFunc(wasm: *Wasm, mod: *Module, func_index: InternPool.Index, air: Air, liveness: Liveness) !void {
    if (build_options.skip_non_native and builtin.object_format != .wasm) {
        @panic("Attempted to compile for object format that was disabled by build configuration");
    }
    if (wasm.llvm_object) |llvm_object| return llvm_object.updateFunc(mod, func_index, air, liveness);

    const tracy = trace(@src());
    defer tracy.end();

    const func = mod.funcInfo(func_index);
    const decl_index = func.owner_decl;
    const decl = mod.declPtr(decl_index);
    const atom_index = try wasm.getOrCreateAtomForDecl(decl_index);
    const atom = wasm.getAtomPtr(atom_index);
    atom.clear();

    // var decl_state: ?Dwarf.DeclState = if (wasm.dwarf) |*dwarf| try dwarf.initDeclState(mod, decl_index) else null;
    // defer if (decl_state) |*ds| ds.deinit();

    var code_writer = std.ArrayList(u8).init(wasm.base.allocator);
    defer code_writer.deinit();
    // const result = try codegen.generateFunction(
    //     &wasm.base,
    //     decl.srcLoc(mod),
    //     func,
    //     air,
    //     liveness,
    //     &code_writer,
    //     if (decl_state) |*ds| .{ .dwarf = ds } else .none,
    // );
    const result = try codegen.generateFunction(
        &wasm.base,
        decl.srcLoc(mod),
        func_index,
        air,
        liveness,
        &code_writer,
        .none,
    );

    const code = switch (result) {
        .ok => code_writer.items,
        .fail => |em| {
            decl.analysis = .codegen_failure;
            try mod.failed_decls.put(mod.gpa, decl_index, em);
            return;
        },
    };

    // if (wasm.dwarf) |*dwarf| {
    //     try dwarf.commitDeclState(
    //         mod,
    //         decl_index,
    //         // Actual value will be written after relocation.
    //         // For Wasm, this is the offset relative to the code section
    //         // which isn't known until flush().
    //         0,
    //         code.len,
    //         &decl_state.?,
    //     );
    // }
    return wasm.finishUpdateDecl(decl_index, code);
}

// Generate code for the Decl, storing it in memory to be later written to
// the file on flush().
pub fn updateDecl(wasm: *Wasm, mod: *Module, decl_index: Module.Decl.Index) !void {
    if (build_options.skip_non_native and builtin.object_format != .wasm) {
        @panic("Attempted to compile for object format that was disabled by build configuration");
    }
    if (wasm.llvm_object) |llvm_object| return llvm_object.updateDecl(mod, decl_index);

    const tracy = trace(@src());
    defer tracy.end();

    const decl = mod.declPtr(decl_index);
    if (decl.val.getFunction(mod)) |_| {
        return;
    } else if (decl.val.getExternFunc(mod)) |_| {
        return;
    }

    const atom_index = try wasm.getOrCreateAtomForDecl(decl_index);
    const atom = wasm.getAtomPtr(atom_index);
    atom.clear();

    if (decl.isExtern(mod)) {
        const variable = decl.getOwnedVariable(mod).?;
        const name = mod.intern_pool.stringToSlice(decl.name);
        const lib_name = mod.intern_pool.stringToSliceUnwrap(variable.lib_name);
        return wasm.addOrUpdateImport(name, atom.sym_index, lib_name, null);
    }
    const val = if (decl.val.getVariable(mod)) |variable| variable.init.toValue() else decl.val;

    var code_writer = std.ArrayList(u8).init(wasm.base.allocator);
    defer code_writer.deinit();

    const res = try codegen.generateSymbol(
        &wasm.base,
        decl.srcLoc(mod),
        .{ .ty = decl.ty, .val = val },
        &code_writer,
        .none,
        .{ .parent_atom_index = atom.sym_index },
    );

    const code = switch (res) {
        .ok => code_writer.items,
        .fail => |em| {
            decl.analysis = .codegen_failure;
            try mod.failed_decls.put(mod.gpa, decl_index, em);
            return;
        },
    };

    return wasm.finishUpdateDecl(decl_index, code);
}

pub fn updateDeclLineNumber(wasm: *Wasm, mod: *Module, decl_index: Module.Decl.Index) !void {
    if (wasm.llvm_object) |_| return;
    if (wasm.dwarf) |*dw| {
        const tracy = trace(@src());
        defer tracy.end();

        const decl = mod.declPtr(decl_index);
        const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));

        log.debug("updateDeclLineNumber {s}{*}", .{ decl_name, decl });
        try dw.updateDeclLineNumber(mod, decl_index);
    }
}

fn finishUpdateDecl(wasm: *Wasm, decl_index: Module.Decl.Index, code: []const u8) !void {
    const mod = wasm.base.options.module.?;
    const decl = mod.declPtr(decl_index);
    const atom_index = wasm.decls.get(decl_index).?;
    const atom = wasm.getAtomPtr(atom_index);
    const symbol = &wasm.symbols.items[atom.sym_index];
    const full_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
    symbol.name = try wasm.string_table.put(wasm.base.allocator, full_name);
    try atom.code.appendSlice(wasm.base.allocator, code);
    try wasm.resolved_symbols.put(wasm.base.allocator, atom.symbolLoc(), {});

    atom.size = @as(u32, @intCast(code.len));
    if (code.len == 0) return;
    atom.alignment = decl.getAlignment(mod);
}

/// From a given symbol location, returns its `wasm.GlobalType`.
/// Asserts the Symbol represents a global.
fn getGlobalType(wasm: *const Wasm, loc: SymbolLoc) std.wasm.GlobalType {
    const symbol = loc.getSymbol(wasm);
    assert(symbol.tag == .global);
    const is_undefined = symbol.isUndefined();
    if (loc.file) |file_index| {
        const obj: Object = wasm.objects.items[file_index];
        if (is_undefined) {
            return obj.findImport(.global, symbol.index).kind.global;
        }
        const import_global_count = obj.importedCountByKind(.global);
        return obj.globals[symbol.index - import_global_count].global_type;
    }
    if (is_undefined) {
        return wasm.imports.get(loc).?.kind.global;
    }
    return wasm.wasm_globals.items[symbol.index].global_type;
}

/// From a given symbol location, returns its `wasm.Type`.
/// Asserts the Symbol represents a function.
fn getFunctionSignature(wasm: *const Wasm, loc: SymbolLoc) std.wasm.Type {
    const symbol = loc.getSymbol(wasm);
    assert(symbol.tag == .function);
    const is_undefined = symbol.isUndefined();
    if (loc.file) |file_index| {
        const obj: Object = wasm.objects.items[file_index];
        if (is_undefined) {
            const ty_index = obj.findImport(.function, symbol.index).kind.function;
            return obj.func_types[ty_index];
        }
        const import_function_count = obj.importedCountByKind(.function);
        const type_index = obj.functions[symbol.index - import_function_count].type_index;
        return obj.func_types[type_index];
    }
    if (is_undefined) {
        const ty_index = wasm.imports.get(loc).?.kind.function;
        return wasm.func_types.items[ty_index];
    }
    return wasm.func_types.items[wasm.functions.get(.{ .file = loc.file, .index = loc.index }).?.type_index];
}

/// Lowers a constant typed value to a local symbol and atom.
/// Returns the symbol index of the local
/// The given `decl` is the parent decl whom owns the constant.
pub fn lowerUnnamedConst(wasm: *Wasm, tv: TypedValue, decl_index: Module.Decl.Index) !u32 {
    const mod = wasm.base.options.module.?;
    assert(tv.ty.zigTypeTag(mod) != .Fn); // cannot create local symbols for functions
    const decl = mod.declPtr(decl_index);

    // Create and initialize a new local symbol and atom
    const atom_index = try wasm.createAtom();
    const parent_atom_index = try wasm.getOrCreateAtomForDecl(decl_index);
    const parent_atom = wasm.getAtomPtr(parent_atom_index);
    const local_index = parent_atom.locals.items.len;
    try parent_atom.locals.append(wasm.base.allocator, atom_index);
    const fqn = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
    const name = try std.fmt.allocPrintZ(wasm.base.allocator, "__unnamed_{s}_{d}", .{
        fqn, local_index,
    });
    defer wasm.base.allocator.free(name);
    var value_bytes = std.ArrayList(u8).init(wasm.base.allocator);
    defer value_bytes.deinit();

    const code = code: {
        const atom = wasm.getAtomPtr(atom_index);
        atom.alignment = tv.ty.abiAlignment(mod);
        wasm.symbols.items[atom.sym_index] = .{
            .name = try wasm.string_table.put(wasm.base.allocator, name),
            .flags = @intFromEnum(Symbol.Flag.WASM_SYM_BINDING_LOCAL),
            .tag = .data,
            .index = undefined,
            .virtual_address = undefined,
        };
        try wasm.resolved_symbols.putNoClobber(wasm.base.allocator, atom.symbolLoc(), {});

        const result = try codegen.generateSymbol(
            &wasm.base,
            decl.srcLoc(mod),
            tv,
            &value_bytes,
            .none,
            .{
                .parent_atom_index = atom.sym_index,
                .addend = null,
            },
        );
        break :code switch (result) {
            .ok => value_bytes.items,
            .fail => |em| {
                decl.analysis = .codegen_failure;
                try mod.failed_decls.put(mod.gpa, decl_index, em);
                return error.CodegenFail;
            },
        };
    };

    const atom = wasm.getAtomPtr(atom_index);
    atom.size = @as(u32, @intCast(code.len));
    try atom.code.appendSlice(wasm.base.allocator, code);
    return atom.sym_index;
}

/// Returns the symbol index from a symbol of which its flag is set global,
/// such as an exported or imported symbol.
/// If the symbol does not yet exist, creates a new one symbol instead
/// and then returns the index to it.
pub fn getGlobalSymbol(wasm: *Wasm, name: []const u8, lib_name: ?[]const u8) !u32 {
    _ = lib_name;
    const name_index = try wasm.string_table.put(wasm.base.allocator, name);
    const gop = try wasm.globals.getOrPut(wasm.base.allocator, name_index);
    if (gop.found_existing) {
        return gop.value_ptr.*.index;
    }

    var symbol: Symbol = .{
        .name = name_index,
        .flags = 0,
        .index = undefined, // index to type will be set after merging function symbols
        .tag = .function,
        .virtual_address = undefined,
    };
    symbol.setGlobal(true);
    symbol.setUndefined(true);

    const sym_index = if (wasm.symbols_free_list.popOrNull()) |index| index else blk: {
        var index = @as(u32, @intCast(wasm.symbols.items.len));
        try wasm.symbols.ensureUnusedCapacity(wasm.base.allocator, 1);
        wasm.symbols.items.len += 1;
        break :blk index;
    };
    wasm.symbols.items[sym_index] = symbol;
    gop.value_ptr.* = .{ .index = sym_index, .file = null };
    try wasm.resolved_symbols.put(wasm.base.allocator, gop.value_ptr.*, {});
    try wasm.undefs.putNoClobber(wasm.base.allocator, name_index, gop.value_ptr.*);
    return sym_index;
}

/// For a given decl, find the given symbol index's atom, and create a relocation for the type.
/// Returns the given pointer address
pub fn getDeclVAddr(
    wasm: *Wasm,
    decl_index: Module.Decl.Index,
    reloc_info: link.File.RelocInfo,
) !u64 {
    const mod = wasm.base.options.module.?;
    const decl = mod.declPtr(decl_index);

    const target_atom_index = try wasm.getOrCreateAtomForDecl(decl_index);
    const target_symbol_index = wasm.getAtom(target_atom_index).sym_index;

    assert(reloc_info.parent_atom_index != 0);
    const atom_index = wasm.symbol_atom.get(.{ .file = null, .index = reloc_info.parent_atom_index }).?;
    const atom = wasm.getAtomPtr(atom_index);
    const is_wasm32 = wasm.base.options.target.cpu.arch == .wasm32;
    if (decl.ty.zigTypeTag(mod) == .Fn) {
        assert(reloc_info.addend == 0); // addend not allowed for function relocations
        // We found a function pointer, so add it to our table,
        // as function pointers are not allowed to be stored inside the data section.
        // They are instead stored in a function table which are called by index.
        try wasm.addTableFunction(target_symbol_index);
        try atom.relocs.append(wasm.base.allocator, .{
            .index = target_symbol_index,
            .offset = @as(u32, @intCast(reloc_info.offset)),
            .relocation_type = if (is_wasm32) .R_WASM_TABLE_INDEX_I32 else .R_WASM_TABLE_INDEX_I64,
        });
    } else {
        try atom.relocs.append(wasm.base.allocator, .{
            .index = target_symbol_index,
            .offset = @as(u32, @intCast(reloc_info.offset)),
            .relocation_type = if (is_wasm32) .R_WASM_MEMORY_ADDR_I32 else .R_WASM_MEMORY_ADDR_I64,
            .addend = @as(i32, @intCast(reloc_info.addend)),
        });
    }
    // we do not know the final address at this point,
    // as atom allocation will determine the address and relocations
    // will calculate and rewrite this. Therefore, we simply return the symbol index
    // that was targeted.
    return target_symbol_index;
}

pub fn deleteDeclExport(wasm: *Wasm, decl_index: Module.Decl.Index) void {
    if (wasm.llvm_object) |_| return;
    const atom_index = wasm.decls.get(decl_index) orelse return;
    const sym_index = wasm.getAtom(atom_index).sym_index;
    const loc: SymbolLoc = .{ .file = null, .index = sym_index };
    const symbol = loc.getSymbol(wasm);
    const symbol_name = wasm.string_table.get(symbol.name);
    log.debug("Deleting export for decl '{s}'", .{symbol_name});
    if (wasm.export_names.fetchRemove(loc)) |kv| {
        assert(wasm.globals.remove(kv.value));
    } else {
        assert(wasm.globals.remove(symbol.name));
    }
}

pub fn updateDeclExports(
    wasm: *Wasm,
    mod: *Module,
    decl_index: Module.Decl.Index,
    exports: []const *Module.Export,
) !void {
    if (build_options.skip_non_native and builtin.object_format != .wasm) {
        @panic("Attempted to compile for object format that was disabled by build configuration");
    }
    if (wasm.llvm_object) |llvm_object| return llvm_object.updateDeclExports(mod, decl_index, exports);

    if (wasm.base.options.emit == null) return;

    const decl = mod.declPtr(decl_index);
    const atom_index = try wasm.getOrCreateAtomForDecl(decl_index);
    const atom = wasm.getAtom(atom_index);
    const atom_sym = atom.symbolLoc().getSymbol(wasm).*;
    const gpa = mod.gpa;

    for (exports) |exp| {
        if (mod.intern_pool.stringToSliceUnwrap(exp.opts.section)) |section| {
            try mod.failed_exports.putNoClobber(gpa, exp, try Module.ErrorMsg.create(
                gpa,
                decl.srcLoc(mod),
                "Unimplemented: ExportOptions.section '{s}'",
                .{section},
            ));
            continue;
        }

        const exported_atom_index = try wasm.getOrCreateAtomForDecl(exp.exported_decl);
        const exported_atom = wasm.getAtom(exported_atom_index);
        const export_name = try wasm.string_table.put(wasm.base.allocator, mod.intern_pool.stringToSlice(exp.opts.name));
        const sym_loc = exported_atom.symbolLoc();
        const symbol = sym_loc.getSymbol(wasm);
        symbol.setGlobal(true);
        symbol.setUndefined(false);
        symbol.index = atom_sym.index;
        symbol.tag = atom_sym.tag;
        symbol.name = atom_sym.name;

        switch (exp.opts.linkage) {
            .Internal => {
                symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
                symbol.setFlag(.WASM_SYM_BINDING_WEAK);
            },
            .Weak => {
                symbol.setFlag(.WASM_SYM_BINDING_WEAK);
            },
            .Strong => {}, // symbols are strong by default
            .LinkOnce => {
                try mod.failed_exports.putNoClobber(gpa, exp, try Module.ErrorMsg.create(
                    gpa,
                    decl.srcLoc(mod),
                    "Unimplemented: LinkOnce",
                    .{},
                ));
                continue;
            },
        }

        if (wasm.globals.get(export_name)) |existing_loc| {
            if (existing_loc.index == atom.sym_index) continue;
            const existing_sym: Symbol = existing_loc.getSymbol(wasm).*;

            if (!existing_sym.isUndefined()) blk: {
                if (symbol.isWeak()) {
                    try wasm.discarded.put(wasm.base.allocator, existing_loc, sym_loc);
                    continue; // to-be-exported symbol is weak, so we keep the existing symbol
                }

                // new symbol is not weak while existing is, replace existing symbol
                if (existing_sym.isWeak()) {
                    break :blk;
                }
                // When both the to-be-exported symbol and the already existing symbol
                // are strong symbols, we have a linker error.
                // In the other case we replace one with the other.
                try mod.failed_exports.put(gpa, exp, try Module.ErrorMsg.create(
                    gpa,
                    decl.srcLoc(mod),
                    \\LinkError: symbol '{}' defined multiple times
                    \\  first definition in '{s}'
                    \\  next definition in '{s}'
                ,
                    .{ exp.opts.name.fmt(&mod.intern_pool), wasm.name, wasm.name },
                ));
                continue;
            }

            // in this case the existing symbol must be replaced either because it's weak or undefined.
            try wasm.discarded.put(wasm.base.allocator, existing_loc, sym_loc);
            _ = wasm.imports.remove(existing_loc);
            _ = wasm.undefs.swapRemove(existing_sym.name);
        }

        // Ensure the symbol will be exported using the given name
        if (!mod.intern_pool.stringEqlSlice(exp.opts.name, sym_loc.getName(wasm))) {
            try wasm.export_names.put(wasm.base.allocator, sym_loc, export_name);
        }

        try wasm.globals.put(
            wasm.base.allocator,
            export_name,
            sym_loc,
        );
    }
}

pub fn freeDecl(wasm: *Wasm, decl_index: Module.Decl.Index) void {
    if (wasm.llvm_object) |llvm_object| return llvm_object.freeDecl(decl_index);
    const mod = wasm.base.options.module.?;
    const decl = mod.declPtr(decl_index);
    const atom_index = wasm.decls.get(decl_index).?;
    const atom = wasm.getAtomPtr(atom_index);
    wasm.symbols_free_list.append(wasm.base.allocator, atom.sym_index) catch {};
    _ = wasm.decls.remove(decl_index);
    wasm.symbols.items[atom.sym_index].tag = .dead;
    for (atom.locals.items) |local_atom_index| {
        const local_atom = wasm.getAtom(local_atom_index);
        const local_symbol = &wasm.symbols.items[local_atom.sym_index];
        local_symbol.tag = .dead; // also for any local symbol
        wasm.symbols_free_list.append(wasm.base.allocator, local_atom.sym_index) catch {};
        assert(wasm.resolved_symbols.swapRemove(local_atom.symbolLoc()));
        assert(wasm.symbol_atom.remove(local_atom.symbolLoc()));
    }

    if (decl.isExtern(mod)) {
        _ = wasm.imports.remove(atom.symbolLoc());
    }
    _ = wasm.resolved_symbols.swapRemove(atom.symbolLoc());
    _ = wasm.symbol_atom.remove(atom.symbolLoc());

    // if (wasm.dwarf) |*dwarf| {
    //     dwarf.freeDecl(decl_index);
    // }

    if (atom.next) |next_atom_index| {
        const next_atom = wasm.getAtomPtr(next_atom_index);
        next_atom.prev = atom.prev;
        atom.next = null;
    }
    if (atom.prev) |prev_index| {
        const prev_atom = wasm.getAtomPtr(prev_index);
        prev_atom.next = atom.next;
        atom.prev = null;
    }
}

/// Appends a new entry to the indirect function table
pub fn addTableFunction(wasm: *Wasm, symbol_index: u32) !void {
    const index = @as(u32, @intCast(wasm.function_table.count()));
    try wasm.function_table.put(wasm.base.allocator, .{ .file = null, .index = symbol_index }, index);
}

/// Assigns indexes to all indirect functions.
/// Starts at offset 1, where the value `0` represents an unresolved function pointer
/// or null-pointer
fn mapFunctionTable(wasm: *Wasm) void {
    var it = wasm.function_table.valueIterator();
    var index: u32 = 1;
    while (it.next()) |value_ptr| : (index += 1) {
        value_ptr.* = index;
    }

    if (wasm.base.options.import_table or wasm.base.options.output_mode == .Obj) {
        const sym_loc = wasm.findGlobalSymbol("__indirect_function_table").?;
        const import = wasm.imports.getPtr(sym_loc).?;
        import.kind.table.limits.min = index - 1; // we start at index 1.
    } else if (index > 1) {
        log.debug("Appending indirect function table", .{});
        const sym_loc = wasm.findGlobalSymbol("__indirect_function_table").?;
        const symbol = sym_loc.getSymbol(wasm);
        const table = &wasm.tables.items[symbol.index - wasm.imported_tables_count];
        table.limits = .{ .min = index, .max = index, .flags = 0x1 };
    }
}

/// Either creates a new import, or updates one if existing.
/// When `type_index` is non-null, we assume an external function.
/// In all other cases, a data-symbol will be created instead.
pub fn addOrUpdateImport(
    wasm: *Wasm,
    /// Name of the import
    name: []const u8,
    /// Symbol index that is external
    symbol_index: u32,
    /// Optional library name (i.e. `extern "c" fn foo() void`
    lib_name: ?[:0]const u8,
    /// The index of the type that represents the function signature
    /// when the extern is a function. When this is null, a data-symbol
    /// is asserted instead.
    type_index: ?u32,
) !void {
    assert(symbol_index != 0);
    // For the import name, we use the decl's name, rather than the fully qualified name
    // Also mangle the name when the lib name is set and not equal to "C" so imports with the same
    // name but different module can be resolved correctly.
    const mangle_name = lib_name != null and
        !std.mem.eql(u8, lib_name.?, "c");
    const full_name = if (mangle_name) full_name: {
        break :full_name try std.fmt.allocPrint(wasm.base.allocator, "{s}|{s}", .{ name, lib_name.? });
    } else name;
    defer if (mangle_name) wasm.base.allocator.free(full_name);

    const decl_name_index = try wasm.string_table.put(wasm.base.allocator, full_name);
    const symbol: *Symbol = &wasm.symbols.items[symbol_index];
    symbol.setUndefined(true);
    symbol.setGlobal(true);
    symbol.name = decl_name_index;
    if (mangle_name) {
        // we specified a specific name for the symbol that does not match the import name
        symbol.setFlag(.WASM_SYM_EXPLICIT_NAME);
    }
    const global_gop = try wasm.globals.getOrPut(wasm.base.allocator, decl_name_index);
    if (!global_gop.found_existing) {
        const loc: SymbolLoc = .{ .file = null, .index = symbol_index };
        global_gop.value_ptr.* = loc;
        try wasm.resolved_symbols.put(wasm.base.allocator, loc, {});
        try wasm.undefs.putNoClobber(wasm.base.allocator, decl_name_index, loc);
    } else if (global_gop.value_ptr.*.index != symbol_index) {
        // We are not updating a symbol, but found an existing global
        // symbol with the same name. This means we always favor the
        // existing symbol, regardless whether it's defined or not.
        // We can also skip storing the import as we will not output
        // this symbol.
        return wasm.discarded.put(
            wasm.base.allocator,
            .{ .file = null, .index = symbol_index },
            global_gop.value_ptr.*,
        );
    }

    if (type_index) |ty_index| {
        const gop = try wasm.imports.getOrPut(wasm.base.allocator, .{ .index = symbol_index, .file = null });
        const module_name = if (lib_name) |l_name| blk: {
            break :blk l_name;
        } else wasm.host_name;
        if (!gop.found_existing) {
            gop.value_ptr.* = .{
                .module_name = try wasm.string_table.put(wasm.base.allocator, module_name),
                .name = try wasm.string_table.put(wasm.base.allocator, name),
                .kind = .{ .function = ty_index },
            };
        }
    } else {
        // non-functions will not be imported from the runtime, but only resolved during link-time
        symbol.tag = .data;
    }
}

/// Kind represents the type of an Atom, which is only
/// used to parse a decl into an Atom to define in which section
/// or segment it should be placed.
const Kind = union(enum) {
    /// Represents the segment the data symbol should
    /// be inserted into.
    /// TODO: Add TLS segments
    data: enum {
        read_only,
        uninitialized,
        initialized,
    },
    function: void,

    /// Returns the segment name the data kind represents.
    /// Asserts `kind` has its active tag set to `data`.
    fn segmentName(kind: Kind) []const u8 {
        switch (kind.data) {
            .read_only => return ".rodata.",
            .uninitialized => return ".bss.",
            .initialized => return ".data.",
        }
    }
};

/// Parses an Atom and inserts its metadata into the corresponding sections.
fn parseAtom(wasm: *Wasm, atom_index: Atom.Index, kind: Kind) !void {
    const atom = wasm.getAtomPtr(atom_index);
    const symbol = (SymbolLoc{ .file = null, .index = atom.sym_index }).getSymbol(wasm);
    const final_index: u32 = switch (kind) {
        .function => result: {
            const index = @as(u32, @intCast(wasm.functions.count() + wasm.imported_functions_count));
            const type_index = wasm.atom_types.get(atom_index).?;
            try wasm.functions.putNoClobber(
                wasm.base.allocator,
                .{ .file = null, .index = index },
                .{ .type_index = type_index },
            );
            symbol.tag = .function;
            symbol.index = index;

            if (wasm.code_section_index == null) {
                wasm.code_section_index = @as(u32, @intCast(wasm.segments.items.len));
                try wasm.segments.append(wasm.base.allocator, .{
                    .alignment = atom.alignment,
                    .size = atom.size,
                    .offset = 0,
                    .flags = 0,
                });
            }

            break :result wasm.code_section_index.?;
        },
        .data => result: {
            const segment_name = try std.mem.concat(wasm.base.allocator, u8, &.{
                kind.segmentName(),
                wasm.string_table.get(symbol.name),
            });
            errdefer wasm.base.allocator.free(segment_name);
            const segment_info: types.Segment = .{
                .name = segment_name,
                .alignment = atom.alignment,
                .flags = 0,
            };
            symbol.tag = .data;

            // when creating an object file, or importing memory and the data belongs in the .bss segment
            // we set the entire region of it to zeroes.
            // We do not have to do this when exporting the memory (the default) because the runtime
            // will do it for us, and we do not emit the bss segment at all.
            if ((wasm.base.options.output_mode == .Obj or wasm.base.options.import_memory) and kind.data == .uninitialized) {
                @memset(atom.code.items, 0);
            }

            const should_merge = wasm.base.options.output_mode != .Obj;
            const gop = try wasm.data_segments.getOrPut(wasm.base.allocator, segment_info.outputName(should_merge));
            if (gop.found_existing) {
                const index = gop.value_ptr.*;
                wasm.segments.items[index].size += atom.size;

                symbol.index = @as(u32, @intCast(wasm.segment_info.getIndex(index).?));
                // segment info already exists, so free its memory
                wasm.base.allocator.free(segment_name);
                break :result index;
            } else {
                const index = @as(u32, @intCast(wasm.segments.items.len));
                var flags: u32 = 0;
                if (wasm.base.options.shared_memory) {
                    flags |= @intFromEnum(Segment.Flag.WASM_DATA_SEGMENT_IS_PASSIVE);
                }
                try wasm.segments.append(wasm.base.allocator, .{
                    .alignment = atom.alignment,
                    .size = 0,
                    .offset = 0,
                    .flags = flags,
                });
                gop.value_ptr.* = index;

                const info_index = @as(u32, @intCast(wasm.segment_info.count()));
                try wasm.segment_info.put(wasm.base.allocator, index, segment_info);
                symbol.index = info_index;
                break :result index;
            }
        },
    };

    const segment: *Segment = &wasm.segments.items[final_index];
    segment.alignment = @max(segment.alignment, atom.alignment);

    try wasm.appendAtomAtIndex(final_index, atom_index);
}

/// From a given index, append the given `Atom` at the back of the linked list.
/// Simply inserts it into the map of atoms when it doesn't exist yet.
pub fn appendAtomAtIndex(wasm: *Wasm, index: u32, atom_index: Atom.Index) !void {
    const atom = wasm.getAtomPtr(atom_index);
    if (wasm.atoms.getPtr(index)) |last_index_ptr| {
        const last = wasm.getAtomPtr(last_index_ptr.*);
        last.*.next = atom_index;
        atom.prev = last_index_ptr.*;
        last_index_ptr.* = atom_index;
    } else {
        try wasm.atoms.putNoClobber(wasm.base.allocator, index, atom_index);
    }
}

/// Allocates debug atoms into their respective debug sections
/// to merge them with maybe-existing debug atoms from object files.
fn allocateDebugAtoms(wasm: *Wasm) !void {
    if (wasm.dwarf == null) return;

    const allocAtom = struct {
        fn f(bin: *Wasm, maybe_index: *?u32, atom_index: Atom.Index) !void {
            const index = maybe_index.* orelse idx: {
                const index = @as(u32, @intCast(bin.segments.items.len));
                try bin.appendDummySegment();
                maybe_index.* = index;
                break :idx index;
            };
            const atom = bin.getAtomPtr(atom_index);
            atom.size = @as(u32, @intCast(atom.code.items.len));
            bin.symbols.items[atom.sym_index].index = index;
            try bin.appendAtomAtIndex(index, atom_index);
        }
    }.f;

    try allocAtom(wasm, &wasm.debug_info_index, wasm.debug_info_atom.?);
    try allocAtom(wasm, &wasm.debug_line_index, wasm.debug_line_atom.?);
    try allocAtom(wasm, &wasm.debug_loc_index, wasm.debug_loc_atom.?);
    try allocAtom(wasm, &wasm.debug_str_index, wasm.debug_str_atom.?);
    try allocAtom(wasm, &wasm.debug_ranges_index, wasm.debug_ranges_atom.?);
    try allocAtom(wasm, &wasm.debug_abbrev_index, wasm.debug_abbrev_atom.?);
    try allocAtom(wasm, &wasm.debug_pubnames_index, wasm.debug_pubnames_atom.?);
    try allocAtom(wasm, &wasm.debug_pubtypes_index, wasm.debug_pubtypes_atom.?);
}

fn allocateAtoms(wasm: *Wasm) !void {
    // first sort the data segments
    try sortDataSegments(wasm);
    try allocateDebugAtoms(wasm);

    var it = wasm.atoms.iterator();
    while (it.next()) |entry| {
        const segment = &wasm.segments.items[entry.key_ptr.*];
        var atom_index = entry.value_ptr.*;
        var offset: u32 = 0;
        while (true) {
            const atom = wasm.getAtomPtr(atom_index);
            const symbol_loc = atom.symbolLoc();
            if (wasm.code_section_index) |index| {
                if (index == entry.key_ptr.*) {
                    if (!wasm.resolved_symbols.contains(symbol_loc)) {
                        // only allocate resolved function body's.
                        atom_index = atom.prev orelse break;
                        continue;
                    }
                }
            }
            offset = std.mem.alignForward(u32, offset, atom.alignment);
            atom.offset = offset;
            log.debug("Atom '{s}' allocated from 0x{x:0>8} to 0x{x:0>8} size={d}", .{
                symbol_loc.getName(wasm),
                offset,
                offset + atom.size,
                atom.size,
            });
            offset += atom.size;
            atom_index = atom.prev orelse break;
        }
        segment.size = std.mem.alignForward(u32, offset, segment.alignment);
    }
}

/// For each data symbol, sets the virtual address.
fn allocateVirtualAddresses(wasm: *Wasm) void {
    for (wasm.resolved_symbols.keys()) |loc| {
        const symbol = loc.getSymbol(wasm);
        if (symbol.tag != .data) {
            continue; // only data symbols have virtual addresses
        }
        const atom_index = wasm.symbol_atom.get(loc) orelse {
            // synthetic symbol that does not contain an atom
            continue;
        };

        const atom = wasm.getAtom(atom_index);
        const merge_segment = wasm.base.options.output_mode != .Obj;
        const segment_info = if (atom.file) |object_index| blk: {
            break :blk wasm.objects.items[object_index].segment_info;
        } else wasm.segment_info.values();
        const segment_name = segment_info[symbol.index].outputName(merge_segment);
        const segment_index = wasm.data_segments.get(segment_name).?;
        const segment = wasm.segments.items[segment_index];

        // TLS symbols have their virtual address set relative to their own TLS segment,
        // rather than the entire Data section.
        if (symbol.hasFlag(.WASM_SYM_TLS)) {
            symbol.virtual_address = atom.offset;
        } else {
            symbol.virtual_address = atom.offset + segment.offset;
        }
    }
}

fn sortDataSegments(wasm: *Wasm) !void {
    var new_mapping: std.StringArrayHashMapUnmanaged(u32) = .{};
    try new_mapping.ensureUnusedCapacity(wasm.base.allocator, wasm.data_segments.count());
    errdefer new_mapping.deinit(wasm.base.allocator);

    const keys = try wasm.base.allocator.dupe([]const u8, wasm.data_segments.keys());
    defer wasm.base.allocator.free(keys);

    const SortContext = struct {
        fn sort(_: void, lhs: []const u8, rhs: []const u8) bool {
            return order(lhs) < order(rhs);
        }

        fn order(name: []const u8) u8 {
            if (mem.startsWith(u8, name, ".rodata")) return 0;
            if (mem.startsWith(u8, name, ".data")) return 1;
            if (mem.startsWith(u8, name, ".text")) return 2;
            return 3;
        }
    };

    mem.sort([]const u8, keys, {}, SortContext.sort);
    for (keys) |key| {
        const segment_index = wasm.data_segments.get(key).?;
        new_mapping.putAssumeCapacity(key, segment_index);
    }
    wasm.data_segments.deinit(wasm.base.allocator);
    wasm.data_segments = new_mapping;
}

/// Obtains all initfuncs from each object file, verifies its function signature,
/// and then appends it to our final `init_funcs` list.
/// After all functions have been inserted, the functions will be ordered based
/// on their priority.
/// NOTE: This function must be called before we merged any other section.
/// This is because all init funcs in the object files contain references to the
/// original functions and their types. We need to know the type to verify it doesn't
/// contain any parameters.
fn setupInitFunctions(wasm: *Wasm) !void {
    for (wasm.objects.items, 0..) |object, file_index| {
        try wasm.init_funcs.ensureUnusedCapacity(wasm.base.allocator, object.init_funcs.len);
        for (object.init_funcs) |init_func| {
            const symbol = object.symtable[init_func.symbol_index];
            const ty: std.wasm.Type = if (symbol.isUndefined()) ty: {
                const imp: types.Import = object.findImport(.function, symbol.index);
                break :ty object.func_types[imp.kind.function];
            } else ty: {
                const func_index = symbol.index - object.importedCountByKind(.function);
                const func = object.functions[func_index];
                break :ty object.func_types[func.type_index];
            };
            if (ty.params.len != 0) {
                log.err("constructor functions cannot take arguments: '{s}'", .{object.string_table.get(symbol.name)});
                return error.InvalidInitFunc;
            }
            log.debug("appended init func '{s}'\n", .{object.string_table.get(symbol.name)});
            wasm.init_funcs.appendAssumeCapacity(.{
                .index = init_func.symbol_index,
                .file = @as(u16, @intCast(file_index)),
                .priority = init_func.priority,
            });
        }
    }

    // sort the initfunctions based on their priority
    mem.sort(InitFuncLoc, wasm.init_funcs.items, {}, InitFuncLoc.lessThan);
}

/// Generates an atom containing the global error set' size.
/// This will only be generated if the symbol exists.
fn setupErrorsLen(wasm: *Wasm) !void {
    const loc = wasm.findGlobalSymbol("__zig_errors_len") orelse return;

    const errors_len = wasm.base.options.module.?.global_error_set.count();
    // overwrite existing atom if it already exists (maybe the error set has increased)
    // if not, allcoate a new atom.
    const atom_index = if (wasm.symbol_atom.get(loc)) |index| blk: {
        const atom = wasm.getAtomPtr(index);
        if (atom.next) |next_atom_index| {
            const next_atom = wasm.getAtomPtr(next_atom_index);
            next_atom.prev = atom.prev;
            atom.next = null;
        }
        if (atom.prev) |prev_index| {
            const prev_atom = wasm.getAtomPtr(prev_index);
            prev_atom.next = atom.next;
            atom.prev = null;
        }
        atom.deinit(wasm);
        break :blk index;
    } else new_atom: {
        const atom_index = @as(Atom.Index, @intCast(wasm.managed_atoms.items.len));
        try wasm.symbol_atom.put(wasm.base.allocator, loc, atom_index);
        try wasm.managed_atoms.append(wasm.base.allocator, undefined);
        break :new_atom atom_index;
    };
    const atom = wasm.getAtomPtr(atom_index);
    atom.* = Atom.empty;
    atom.sym_index = loc.index;
    atom.size = 2;
    try atom.code.writer(wasm.base.allocator).writeIntLittle(u16, @as(u16, @intCast(errors_len)));

    try wasm.parseAtom(atom_index, .{ .data = .read_only });
}

/// Creates a function body for the `__wasm_call_ctors` symbol.
/// Loops over all constructors found in `init_funcs` and calls them
/// respectively based on their priority which was sorted by `setupInitFunctions`.
/// NOTE: This function must be called after we merged all sections to ensure the
/// references to the function stored in the symbol have been finalized so we end
/// up calling the resolved function.
fn initializeCallCtorsFunction(wasm: *Wasm) !void {
    // No code to emit, so also no ctors to call
    if (wasm.code_section_index == null) {
        // Make sure to remove it from the resolved symbols so we do not emit
        // it within any section. TODO: Remove this once we implement garbage collection.
        const loc = wasm.findGlobalSymbol("__wasm_call_ctors").?;
        std.debug.assert(wasm.resolved_symbols.swapRemove(loc));
        return;
    }

    var function_body = std.ArrayList(u8).init(wasm.base.allocator);
    defer function_body.deinit();
    const writer = function_body.writer();

    // Create the function body
    {
        // Write locals count (we have none)
        try leb.writeULEB128(writer, @as(u32, 0));

        // call constructors
        for (wasm.init_funcs.items) |init_func_loc| {
            const symbol = init_func_loc.getSymbol(wasm);
            const func = wasm.functions.values()[symbol.index - wasm.imported_functions_count];
            const ty = wasm.func_types.items[func.type_index];

            // Call function by its function index
            try writer.writeByte(std.wasm.opcode(.call));
            try leb.writeULEB128(writer, symbol.index);

            // drop all returned values from the stack as __wasm_call_ctors has no return value
            for (ty.returns) |_| {
                try writer.writeByte(std.wasm.opcode(.drop));
            }
        }

        // End function body
        try writer.writeByte(std.wasm.opcode(.end));
    }

    try wasm.createSyntheticFunction(
        "__wasm_call_ctors",
        std.wasm.Type{ .params = &.{}, .returns = &.{} },
        &function_body,
    );
}

fn createSyntheticFunction(
    wasm: *Wasm,
    symbol_name: []const u8,
    func_ty: std.wasm.Type,
    function_body: *std.ArrayList(u8),
) !void {
    const loc = wasm.findGlobalSymbol(symbol_name) orelse
        try wasm.createSyntheticSymbol(symbol_name, .function);
    const symbol = loc.getSymbol(wasm);
    const ty_index = try wasm.putOrGetFuncType(func_ty);
    // create function with above type
    const func_index = wasm.imported_functions_count + @as(u32, @intCast(wasm.functions.count()));
    try wasm.functions.putNoClobber(
        wasm.base.allocator,
        .{ .file = null, .index = func_index },
        .{ .type_index = ty_index },
    );
    symbol.index = func_index;

    // create the atom that will be output into the final binary
    const atom_index = @as(Atom.Index, @intCast(wasm.managed_atoms.items.len));
    const atom = try wasm.managed_atoms.addOne(wasm.base.allocator);
    atom.* = .{
        .size = @as(u32, @intCast(function_body.items.len)),
        .offset = 0,
        .sym_index = loc.index,
        .file = null,
        .alignment = 1,
        .next = null,
        .prev = null,
        .code = function_body.moveToUnmanaged(),
    };
    try wasm.appendAtomAtIndex(wasm.code_section_index.?, atom_index);
    try wasm.symbol_atom.putNoClobber(wasm.base.allocator, loc, atom_index);

    // `allocateAtoms` has already been called, set the atom's offset manually.
    // This is fine to do manually as we insert the atom at the very end.
    const prev_atom = wasm.getAtom(atom.prev.?);
    atom.offset = prev_atom.offset + prev_atom.size;
}

/// Unlike `createSyntheticFunction` this function is to be called by
/// the codegeneration backend. This will not allocate the created Atom yet,
/// but will instead be appended to `synthetic_functions` list and will be
/// parsed at the end of code generation.
/// Returns the index of the symbol.
pub fn createFunction(
    wasm: *Wasm,
    symbol_name: []const u8,
    func_ty: std.wasm.Type,
    function_body: *std.ArrayList(u8),
    relocations: *std.ArrayList(Relocation),
) !u32 {
    const loc = try wasm.createSyntheticSymbol(symbol_name, .function);

    const atom_index = @as(Atom.Index, @intCast(wasm.managed_atoms.items.len));
    const atom = try wasm.managed_atoms.addOne(wasm.base.allocator);
    atom.* = .{
        .size = @as(u32, @intCast(function_body.items.len)),
        .offset = 0,
        .sym_index = loc.index,
        .file = null,
        .alignment = 1,
        .next = null,
        .prev = null,
        .code = function_body.moveToUnmanaged(),
        .relocs = relocations.moveToUnmanaged(),
    };
    const symbol = loc.getSymbol(wasm);
    symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN); // ensure function does not get exported

    const section_index = wasm.code_section_index orelse idx: {
        const index = @as(u32, @intCast(wasm.segments.items.len));
        try wasm.appendDummySegment();
        break :idx index;
    };
    try wasm.appendAtomAtIndex(section_index, atom_index);
    try wasm.symbol_atom.putNoClobber(wasm.base.allocator, loc, atom_index);
    try wasm.atom_types.put(wasm.base.allocator, atom_index, try wasm.putOrGetFuncType(func_ty));
    try wasm.synthetic_functions.append(wasm.base.allocator, atom_index);

    return loc.index;
}

/// If required, sets the function index in the `start` section.
fn setupStartSection(wasm: *Wasm) !void {
    if (wasm.findGlobalSymbol("__wasm_init_memory")) |loc| {
        wasm.entry = loc.getSymbol(wasm).index;
    }
}

fn initializeTLSFunction(wasm: *Wasm) !void {
    if (!wasm.base.options.shared_memory) return;

    var function_body = std.ArrayList(u8).init(wasm.base.allocator);
    defer function_body.deinit();
    const writer = function_body.writer();

    // locals
    try writer.writeByte(0);

    // If there's a TLS segment, initialize it during runtime using the bulk-memory feature
    if (wasm.data_segments.getIndex(".tdata")) |data_index| {
        const segment_index = wasm.data_segments.entries.items(.value)[data_index];
        const segment = wasm.segments.items[segment_index];

        const param_local: u32 = 0;

        try writer.writeByte(std.wasm.opcode(.local_get));
        try leb.writeULEB128(writer, param_local);

        const tls_base_loc = wasm.findGlobalSymbol("__tls_base").?;
        try writer.writeByte(std.wasm.opcode(.global_set));
        try leb.writeULEB128(writer, tls_base_loc.getSymbol(wasm).index);

        // load stack values for the bulk-memory operation
        {
            try writer.writeByte(std.wasm.opcode(.local_get));
            try leb.writeULEB128(writer, param_local);

            try writer.writeByte(std.wasm.opcode(.i32_const));
            try leb.writeULEB128(writer, @as(u32, 0)); //segment offset

            try writer.writeByte(std.wasm.opcode(.i32_const));
            try leb.writeULEB128(writer, @as(u32, segment.size)); //segment offset
        }

        // perform the bulk-memory operation to initialize the data segment
        try writer.writeByte(std.wasm.opcode(.misc_prefix));
        try leb.writeULEB128(writer, std.wasm.miscOpcode(.memory_init));
        // segment immediate
        try leb.writeULEB128(writer, @as(u32, @intCast(data_index)));
        // memory index immediate (always 0)
        try leb.writeULEB128(writer, @as(u32, 0));
    }

    // If we have to perform any TLS relocations, call the corresponding function
    // which performs all runtime TLS relocations. This is a synthetic function,
    // generated by the linker.
    if (wasm.findGlobalSymbol("__wasm_apply_global_tls_relocs")) |loc| {
        try writer.writeByte(std.wasm.opcode(.call));
        try leb.writeULEB128(writer, loc.getSymbol(wasm).index);
    }

    try writer.writeByte(std.wasm.opcode(.end));

    try wasm.createSyntheticFunction(
        "__wasm_init_tls",
        std.wasm.Type{ .params = &.{.i32}, .returns = &.{} },
        &function_body,
    );
}

fn setupImports(wasm: *Wasm) !void {
    log.debug("Merging imports", .{});
    var discarded_it = wasm.discarded.keyIterator();
    while (discarded_it.next()) |discarded| {
        if (discarded.file == null) {
            // remove an import if it was resolved
            if (wasm.imports.remove(discarded.*)) {
                log.debug("Removed symbol '{s}' as an import", .{
                    discarded.getName(wasm),
                });
            }
        }
    }

    for (wasm.resolved_symbols.keys()) |symbol_loc| {
        if (symbol_loc.file == null) {
            // imports generated by Zig code are already in the `import` section
            continue;
        }

        const symbol = symbol_loc.getSymbol(wasm);
        if (std.mem.eql(u8, symbol_loc.getName(wasm), "__indirect_function_table")) {
            continue;
        }
        if (!symbol.requiresImport()) {
            continue;
        }

        log.debug("Symbol '{s}' will be imported from the host", .{symbol_loc.getName(wasm)});
        const object = wasm.objects.items[symbol_loc.file.?];
        const import = object.findImport(symbol.tag.externalType(), symbol.index);

        // We copy the import to a new import to ensure the names contain references
        // to the internal string table, rather than of the object file.
        var new_imp: types.Import = .{
            .module_name = try wasm.string_table.put(wasm.base.allocator, object.string_table.get(import.module_name)),
            .name = try wasm.string_table.put(wasm.base.allocator, object.string_table.get(import.name)),
            .kind = import.kind,
        };
        // TODO: De-duplicate imports when they contain the same names and type
        try wasm.imports.putNoClobber(wasm.base.allocator, symbol_loc, new_imp);
    }

    // Assign all indexes of the imports to their representing symbols
    var function_index: u32 = 0;
    var global_index: u32 = 0;
    var table_index: u32 = 0;
    var it = wasm.imports.iterator();
    while (it.next()) |entry| {
        const symbol = entry.key_ptr.*.getSymbol(wasm);
        const import: types.Import = entry.value_ptr.*;
        switch (import.kind) {
            .function => {
                symbol.index = function_index;
                function_index += 1;
            },
            .global => {
                symbol.index = global_index;
                global_index += 1;
            },
            .table => {
                symbol.index = table_index;
                table_index += 1;
            },
            else => unreachable,
        }
    }
    wasm.imported_functions_count = function_index;
    wasm.imported_globals_count = global_index;
    wasm.imported_tables_count = table_index;

    log.debug("Merged ({d}) functions, ({d}) globals, and ({d}) tables into import section", .{
        function_index,
        global_index,
        table_index,
    });
}

/// Takes the global, function and table section from each linked object file
/// and merges it into a single section for each.
fn mergeSections(wasm: *Wasm) !void {
    for (wasm.resolved_symbols.keys()) |sym_loc| {
        if (sym_loc.file == null) {
            // Zig code-generated symbols are already within the sections and do not
            // require to be merged
            continue;
        }

        const object = &wasm.objects.items[sym_loc.file.?];
        const symbol = &object.symtable[sym_loc.index];
        if (symbol.isUndefined() or (symbol.tag != .function and symbol.tag != .global and symbol.tag != .table)) {
            // Skip undefined symbols as they go in the `import` section
            // Also skip symbols that do not need to have a section merged.
            continue;
        }

        const offset = object.importedCountByKind(symbol.tag.externalType());
        const index = symbol.index - offset;
        switch (symbol.tag) {
            .function => {
                const gop = try wasm.functions.getOrPut(
                    wasm.base.allocator,
                    .{ .file = sym_loc.file, .index = symbol.index },
                );
                if (!gop.found_existing) {
                    gop.value_ptr.* = object.functions[index];
                }
                symbol.index = @as(u32, @intCast(gop.index)) + wasm.imported_functions_count;
            },
            .global => {
                const original_global = object.globals[index];
                symbol.index = @as(u32, @intCast(wasm.wasm_globals.items.len)) + wasm.imported_globals_count;
                try wasm.wasm_globals.append(wasm.base.allocator, original_global);
            },
            .table => {
                const original_table = object.tables[index];
                symbol.index = @as(u32, @intCast(wasm.tables.items.len)) + wasm.imported_tables_count;
                try wasm.tables.append(wasm.base.allocator, original_table);
            },
            else => unreachable,
        }
    }

    log.debug("Merged ({d}) functions", .{wasm.functions.count()});
    log.debug("Merged ({d}) globals", .{wasm.wasm_globals.items.len});
    log.debug("Merged ({d}) tables", .{wasm.tables.items.len});
}

/// Merges function types of all object files into the final
/// 'types' section, while assigning the type index to the representing
/// section (import, export, function).
fn mergeTypes(wasm: *Wasm) !void {
    // A map to track which functions have already had their
    // type inserted. If we do this for the same function multiple times,
    // it will be overwritten with the incorrect type.
    var dirty = std.AutoHashMap(u32, void).init(wasm.base.allocator);
    try dirty.ensureUnusedCapacity(@as(u32, @intCast(wasm.functions.count())));
    defer dirty.deinit();

    for (wasm.resolved_symbols.keys()) |sym_loc| {
        if (sym_loc.file == null) {
            // zig code-generated symbols are already present in final type section
            continue;
        }
        const object = wasm.objects.items[sym_loc.file.?];
        const symbol = object.symtable[sym_loc.index];
        if (symbol.tag != .function) {
            // Only functions have types
            continue;
        }

        if (symbol.isUndefined()) {
            log.debug("Adding type from extern function '{s}'", .{sym_loc.getName(wasm)});
            const import: *types.Import = wasm.imports.getPtr(sym_loc) orelse continue;
            const original_type = object.func_types[import.kind.function];
            import.kind.function = try wasm.putOrGetFuncType(original_type);
        } else if (!dirty.contains(symbol.index)) {
            log.debug("Adding type from function '{s}'", .{sym_loc.getName(wasm)});
            const func = &wasm.functions.values()[symbol.index - wasm.imported_functions_count];
            func.type_index = try wasm.putOrGetFuncType(object.func_types[func.type_index]);
            dirty.putAssumeCapacityNoClobber(symbol.index, {});
        }
    }
    log.debug("Completed merging and deduplicating types. Total count: ({d})", .{wasm.func_types.items.len});
}

fn setupExports(wasm: *Wasm) !void {
    if (wasm.base.options.output_mode == .Obj) return;
    log.debug("Building exports from symbols", .{});

    const force_exp_names = wasm.base.options.export_symbol_names;
    if (force_exp_names.len > 0) {
        var failed_exports = false;

        for (force_exp_names) |exp_name| {
            const loc = wasm.findGlobalSymbol(exp_name) orelse {
                log.err("could not export '{s}', symbol not found", .{exp_name});
                failed_exports = true;
                continue;
            };

            const symbol = loc.getSymbol(wasm);
            symbol.setFlag(.WASM_SYM_EXPORTED);
        }

        if (failed_exports) {
            return error.MissingSymbol;
        }
    }

    for (wasm.resolved_symbols.keys()) |sym_loc| {
        const symbol = sym_loc.getSymbol(wasm);
        if (!symbol.isExported(wasm.base.options.rdynamic)) continue;

        const sym_name = sym_loc.getName(wasm);
        const export_name = if (wasm.export_names.get(sym_loc)) |name| name else blk: {
            if (sym_loc.file == null) break :blk symbol.name;
            break :blk try wasm.string_table.put(wasm.base.allocator, sym_name);
        };
        const exp: types.Export = if (symbol.tag == .data) exp: {
            const global_index = @as(u32, @intCast(wasm.imported_globals_count + wasm.wasm_globals.items.len));
            try wasm.wasm_globals.append(wasm.base.allocator, .{
                .global_type = .{ .valtype = .i32, .mutable = false },
                .init = .{ .i32_const = @as(i32, @intCast(symbol.virtual_address)) },
            });
            break :exp .{
                .name = export_name,
                .kind = .global,
                .index = global_index,
            };
        } else .{
            .name = export_name,
            .kind = symbol.tag.externalType(),
            .index = symbol.index,
        };
        log.debug("Exporting symbol '{s}' as '{s}' at index: ({d})", .{
            sym_name,
            wasm.string_table.get(exp.name),
            exp.index,
        });
        try wasm.exports.append(wasm.base.allocator, exp);
    }

    log.debug("Completed building exports. Total count: ({d})", .{wasm.exports.items.len});
}

fn setupStart(wasm: *Wasm) !void {
    const entry_name = wasm.base.options.entry orelse "_start";

    const symbol_loc = wasm.findGlobalSymbol(entry_name) orelse {
        if (wasm.base.options.output_mode == .Exe) {
            if (wasm.base.options.wasi_exec_model == .reactor) return; // Not required for reactors
        } else {
            return; // No entry point needed for non-executable wasm files
        }
        log.err("Entry symbol '{s}' missing", .{entry_name});
        return error.MissingSymbol;
    };

    const symbol = symbol_loc.getSymbol(wasm);
    if (symbol.tag != .function) {
        log.err("Entry symbol '{s}' is not a function", .{entry_name});
        return error.InvalidEntryKind;
    }

    // Ensure the symbol is exported so host environment can access it
    if (wasm.base.options.output_mode != .Obj) {
        symbol.setFlag(.WASM_SYM_EXPORTED);
    }
}

/// Sets up the memory section of the wasm module, as well as the stack.
fn setupMemory(wasm: *Wasm) !void {
    log.debug("Setting up memory layout", .{});
    const page_size = std.wasm.page_size; // 64kb
    // Use the user-provided stack size or else we use 1MB by default
    const stack_size = wasm.base.options.stack_size_override orelse page_size * 16;
    const stack_alignment = 16; // wasm's stack alignment as specified by tool-convention
    const heap_alignment = 16; // wasm's heap alignment as specified by tool-convention

    // Always place the stack at the start by default
    // unless the user specified the global-base flag
    var place_stack_first = true;
    var memory_ptr: u64 = if (wasm.base.options.global_base) |base| blk: {
        place_stack_first = false;
        break :blk base;
    } else 0;

    const is_obj = wasm.base.options.output_mode == .Obj;

    if (place_stack_first and !is_obj) {
        memory_ptr = std.mem.alignForward(u64, memory_ptr, stack_alignment);
        memory_ptr += stack_size;
        // We always put the stack pointer global at index 0
        wasm.wasm_globals.items[0].init.i32_const = @as(i32, @bitCast(@as(u32, @intCast(memory_ptr))));
    }

    var offset: u32 = @as(u32, @intCast(memory_ptr));
    var data_seg_it = wasm.data_segments.iterator();
    while (data_seg_it.next()) |entry| {
        const segment = &wasm.segments.items[entry.value_ptr.*];
        memory_ptr = std.mem.alignForward(u64, memory_ptr, segment.alignment);

        // set TLS-related symbols
        if (mem.eql(u8, entry.key_ptr.*, ".tdata")) {
            if (wasm.findGlobalSymbol("__tls_size")) |loc| {
                const sym = loc.getSymbol(wasm);
                wasm.wasm_globals.items[sym.index - wasm.imported_globals_count].init.i32_const = @intCast(segment.size);
            }
            if (wasm.findGlobalSymbol("__tls_align")) |loc| {
                const sym = loc.getSymbol(wasm);
                wasm.wasm_globals.items[sym.index - wasm.imported_globals_count].init.i32_const = @intCast(segment.alignment);
            }
            if (wasm.findGlobalSymbol("__tls_base")) |loc| {
                const sym = loc.getSymbol(wasm);
                wasm.wasm_globals.items[sym.index - wasm.imported_globals_count].init.i32_const = if (wasm.base.options.shared_memory)
                    @as(i32, 0)
                else
                    @as(i32, @intCast(memory_ptr));
            }
        }

        memory_ptr += segment.size;
        segment.offset = offset;
        offset += segment.size;
    }

    // create the memory init flag which is used by the init memory function
    if (wasm.base.options.shared_memory and wasm.hasPassiveInitializationSegments()) {
        // align to pointer size
        memory_ptr = mem.alignForward(u64, memory_ptr, 4);
        const loc = try wasm.createSyntheticSymbol("__wasm_init_memory_flag", .data);
        const sym = loc.getSymbol(wasm);
        sym.virtual_address = @as(u32, @intCast(memory_ptr));
        memory_ptr += 4;
    }

    if (!place_stack_first and !is_obj) {
        memory_ptr = std.mem.alignForward(u64, memory_ptr, stack_alignment);
        memory_ptr += stack_size;
        wasm.wasm_globals.items[0].init.i32_const = @as(i32, @bitCast(@as(u32, @intCast(memory_ptr))));
    }

    // One of the linked object files has a reference to the __heap_base symbol.
    // We must set its virtual address so it can be used in relocations.
    if (wasm.findGlobalSymbol("__heap_base")) |loc| {
        const symbol = loc.getSymbol(wasm);
        symbol.virtual_address = @as(u32, @intCast(mem.alignForward(u64, memory_ptr, heap_alignment)));
    }

    // Setup the max amount of pages
    // For now we only support wasm32 by setting the maximum allowed memory size 2^32-1
    const max_memory_allowed: u64 = (1 << 32) - 1;

    if (wasm.base.options.initial_memory) |initial_memory| {
        if (!std.mem.isAlignedGeneric(u64, initial_memory, page_size)) {
            log.err("Initial memory must be {d}-byte aligned", .{page_size});
            return error.MissAlignment;
        }
        if (memory_ptr > initial_memory) {
            log.err("Initial memory too small, must be at least {d} bytes", .{memory_ptr});
            return error.MemoryTooSmall;
        }
        if (initial_memory > max_memory_allowed) {
            log.err("Initial memory exceeds maximum memory {d}", .{max_memory_allowed});
            return error.MemoryTooBig;
        }
        memory_ptr = initial_memory;
    }
    memory_ptr = mem.alignForward(u64, memory_ptr, std.wasm.page_size);
    // In case we do not import memory, but define it ourselves,
    // set the minimum amount of pages on the memory section.
    wasm.memories.limits.min = @as(u32, @intCast(memory_ptr / page_size));
    log.debug("Total memory pages: {d}", .{wasm.memories.limits.min});

    if (wasm.findGlobalSymbol("__heap_end")) |loc| {
        const symbol = loc.getSymbol(wasm);
        symbol.virtual_address = @as(u32, @intCast(memory_ptr));
    }

    if (wasm.base.options.max_memory) |max_memory| {
        if (!std.mem.isAlignedGeneric(u64, max_memory, page_size)) {
            log.err("Maximum memory must be {d}-byte aligned", .{page_size});
            return error.MissAlignment;
        }
        if (memory_ptr > max_memory) {
            log.err("Maxmimum memory too small, must be at least {d} bytes", .{memory_ptr});
            return error.MemoryTooSmall;
        }
        if (max_memory > max_memory_allowed) {
            log.err("Maximum memory exceeds maxmium amount {d}", .{max_memory_allowed});
            return error.MemoryTooBig;
        }
        wasm.memories.limits.max = @as(u32, @intCast(max_memory / page_size));
        wasm.memories.limits.setFlag(.WASM_LIMITS_FLAG_HAS_MAX);
        if (wasm.base.options.shared_memory) {
            wasm.memories.limits.setFlag(.WASM_LIMITS_FLAG_IS_SHARED);
        }
        log.debug("Maximum memory pages: {?d}", .{wasm.memories.limits.max});
    }
}

/// From a given object's index and the index of the segment, returns the corresponding
/// index of the segment within the final data section. When the segment does not yet
/// exist, a new one will be initialized and appended. The new index will be returned in that case.
pub fn getMatchingSegment(wasm: *Wasm, object_index: u16, relocatable_index: u32) !?u32 {
    const object: Object = wasm.objects.items[object_index];
    const relocatable_data = object.relocatable_data[relocatable_index];
    const index = @as(u32, @intCast(wasm.segments.items.len));

    switch (relocatable_data.type) {
        .data => {
            const segment_info = object.segment_info[relocatable_data.index];
            const merge_segment = wasm.base.options.output_mode != .Obj;
            const result = try wasm.data_segments.getOrPut(wasm.base.allocator, segment_info.outputName(merge_segment));
            if (!result.found_existing) {
                result.value_ptr.* = index;
                var flags: u32 = 0;
                if (wasm.base.options.shared_memory) {
                    flags |= @intFromEnum(Segment.Flag.WASM_DATA_SEGMENT_IS_PASSIVE);
                }
                try wasm.segments.append(wasm.base.allocator, .{
                    .alignment = 1,
                    .size = 0,
                    .offset = 0,
                    .flags = flags,
                });
                return index;
            } else return result.value_ptr.*;
        },
        .code => return wasm.code_section_index orelse blk: {
            wasm.code_section_index = index;
            try wasm.appendDummySegment();
            break :blk index;
        },
        .debug => {
            const debug_name = object.getDebugName(relocatable_data);
            if (mem.eql(u8, debug_name, ".debug_info")) {
                return wasm.debug_info_index orelse blk: {
                    wasm.debug_info_index = index;
                    try wasm.appendDummySegment();
                    break :blk index;
                };
            } else if (mem.eql(u8, debug_name, ".debug_line")) {
                return wasm.debug_line_index orelse blk: {
                    wasm.debug_line_index = index;
                    try wasm.appendDummySegment();
                    break :blk index;
                };
            } else if (mem.eql(u8, debug_name, ".debug_loc")) {
                return wasm.debug_loc_index orelse blk: {
                    wasm.debug_loc_index = index;
                    try wasm.appendDummySegment();
                    break :blk index;
                };
            } else if (mem.eql(u8, debug_name, ".debug_ranges")) {
                return wasm.debug_line_index orelse blk: {
                    wasm.debug_ranges_index = index;
                    try wasm.appendDummySegment();
                    break :blk index;
                };
            } else if (mem.eql(u8, debug_name, ".debug_pubnames")) {
                return wasm.debug_pubnames_index orelse blk: {
                    wasm.debug_pubnames_index = index;
                    try wasm.appendDummySegment();
                    break :blk index;
                };
            } else if (mem.eql(u8, debug_name, ".debug_pubtypes")) {
                return wasm.debug_pubtypes_index orelse blk: {
                    wasm.debug_pubtypes_index = index;
                    try wasm.appendDummySegment();
                    break :blk index;
                };
            } else if (mem.eql(u8, debug_name, ".debug_abbrev")) {
                return wasm.debug_abbrev_index orelse blk: {
                    wasm.debug_abbrev_index = index;
                    try wasm.appendDummySegment();
                    break :blk index;
                };
            } else if (mem.eql(u8, debug_name, ".debug_str")) {
                return wasm.debug_str_index orelse blk: {
                    wasm.debug_str_index = index;
                    try wasm.appendDummySegment();
                    break :blk index;
                };
            } else {
                log.warn("found unknown debug section '{s}'", .{debug_name});
                log.warn("  debug section will be skipped", .{});
                return null;
            }
        },
    }
}

/// Appends a new segment with default field values
fn appendDummySegment(wasm: *Wasm) !void {
    try wasm.segments.append(wasm.base.allocator, .{
        .alignment = 1,
        .size = 0,
        .offset = 0,
        .flags = 0,
    });
}

/// Returns the symbol index of the error name table.
///
/// When the symbol does not yet exist, it will create a new one instead.
pub fn getErrorTableSymbol(wasm: *Wasm) !u32 {
    if (wasm.error_table_symbol) |symbol| {
        return symbol;
    }

    // no error was referenced yet, so create a new symbol and atom for it
    // and then return said symbol's index. The final table will be populated
    // during `flush` when we know all possible error names.

    const atom_index = try wasm.createAtom();
    const atom = wasm.getAtomPtr(atom_index);
    const slice_ty = Type.slice_const_u8_sentinel_0;
    const mod = wasm.base.options.module.?;
    atom.alignment = slice_ty.abiAlignment(mod);
    const sym_index = atom.sym_index;

    const sym_name = try wasm.string_table.put(wasm.base.allocator, "__zig_err_name_table");
    const symbol = &wasm.symbols.items[sym_index];
    symbol.* = .{
        .name = sym_name,
        .tag = .data,
        .flags = 0,
        .index = 0,
        .virtual_address = undefined,
    };
    symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);

    try wasm.resolved_symbols.put(wasm.base.allocator, atom.symbolLoc(), {});

    log.debug("Error name table was created with symbol index: ({d})", .{sym_index});
    wasm.error_table_symbol = sym_index;
    return sym_index;
}

/// Populates the error name table, when `error_table_symbol` is not null.
///
/// This creates a table that consists of pointers and length to each error name.
/// The table is what is being pointed to within the runtime bodies that are generated.
fn populateErrorNameTable(wasm: *Wasm) !void {
    const symbol_index = wasm.error_table_symbol orelse return;
    const atom_index = wasm.symbol_atom.get(.{ .file = null, .index = symbol_index }).?;

    // Rather than creating a symbol for each individual error name,
    // we create a symbol for the entire region of error names. We then calculate
    // the pointers into the list using addends which are appended to the relocation.
    const names_atom_index = try wasm.createAtom();
    const names_atom = wasm.getAtomPtr(names_atom_index);
    names_atom.alignment = 1;
    const sym_name = try wasm.string_table.put(wasm.base.allocator, "__zig_err_names");
    const names_symbol = &wasm.symbols.items[names_atom.sym_index];
    names_symbol.* = .{
        .name = sym_name,
        .tag = .data,
        .flags = 0,
        .index = 0,
        .virtual_address = undefined,
    };
    names_symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);

    log.debug("Populating error names", .{});

    // Addend for each relocation to the table
    var addend: u32 = 0;
    const mod = wasm.base.options.module.?;
    for (mod.global_error_set.keys()) |error_name_nts| {
        const atom = wasm.getAtomPtr(atom_index);

        const error_name = mod.intern_pool.stringToSlice(error_name_nts);
        const len = @as(u32, @intCast(error_name.len + 1)); // names are 0-termianted

        const slice_ty = Type.slice_const_u8_sentinel_0;
        const offset = @as(u32, @intCast(atom.code.items.len));
        // first we create the data for the slice of the name
        try atom.code.appendNTimes(wasm.base.allocator, 0, 4); // ptr to name, will be relocated
        try atom.code.writer(wasm.base.allocator).writeIntLittle(u32, len - 1);
        // create relocation to the error name
        try atom.relocs.append(wasm.base.allocator, .{
            .index = names_atom.sym_index,
            .relocation_type = .R_WASM_MEMORY_ADDR_I32,
            .offset = offset,
            .addend = @as(i32, @intCast(addend)),
        });
        atom.size += @as(u32, @intCast(slice_ty.abiSize(mod)));
        addend += len;

        // as we updated the error name table, we now store the actual name within the names atom
        try names_atom.code.ensureUnusedCapacity(wasm.base.allocator, len);
        names_atom.code.appendSliceAssumeCapacity(error_name);
        names_atom.code.appendAssumeCapacity(0);

        log.debug("Populated error name: '{s}'", .{error_name});
    }
    names_atom.size = addend;

    const name_loc = names_atom.symbolLoc();
    try wasm.resolved_symbols.put(wasm.base.allocator, name_loc, {});
    try wasm.symbol_atom.put(wasm.base.allocator, name_loc, names_atom_index);

    // link the atoms with the rest of the binary so they can be allocated
    // and relocations will be performed.
    try wasm.parseAtom(atom_index, .{ .data = .read_only });
    try wasm.parseAtom(names_atom_index, .{ .data = .read_only });
}

/// From a given index variable, creates a new debug section.
/// This initializes the index, appends a new segment,
/// and finally, creates a managed `Atom`.
pub fn createDebugSectionForIndex(wasm: *Wasm, index: *?u32, name: []const u8) !Atom.Index {
    const new_index = @as(u32, @intCast(wasm.segments.items.len));
    index.* = new_index;
    try wasm.appendDummySegment();

    const atom_index = try wasm.createAtom();
    const atom = wasm.getAtomPtr(atom_index);
    wasm.symbols.items[atom.sym_index] = .{
        .tag = .section,
        .name = try wasm.string_table.put(wasm.base.allocator, name),
        .index = 0,
        .flags = @intFromEnum(Symbol.Flag.WASM_SYM_BINDING_LOCAL),
    };

    atom.alignment = 1; // debug sections are always 1-byte-aligned
    return atom_index;
}

fn resetState(wasm: *Wasm) void {
    for (wasm.segment_info.values()) |segment_info| {
        wasm.base.allocator.free(segment_info.name);
    }

    var atom_it = wasm.decls.valueIterator();
    while (atom_it.next()) |atom_index| {
        const atom = wasm.getAtomPtr(atom_index.*);
        atom.next = null;
        atom.prev = null;

        for (atom.locals.items) |local_atom_index| {
            const local_atom = wasm.getAtomPtr(local_atom_index);
            local_atom.next = null;
            local_atom.prev = null;
        }
    }

    wasm.functions.clearRetainingCapacity();
    wasm.exports.clearRetainingCapacity();
    wasm.segments.clearRetainingCapacity();
    wasm.segment_info.clearRetainingCapacity();
    wasm.data_segments.clearRetainingCapacity();
    wasm.atoms.clearRetainingCapacity();
    wasm.symbol_atom.clearRetainingCapacity();
    wasm.code_section_index = null;
    wasm.debug_info_index = null;
    wasm.debug_line_index = null;
    wasm.debug_loc_index = null;
    wasm.debug_str_index = null;
    wasm.debug_ranges_index = null;
    wasm.debug_abbrev_index = null;
    wasm.debug_pubnames_index = null;
    wasm.debug_pubtypes_index = null;
}

pub fn flush(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) link.File.FlushError!void {
    if (wasm.base.options.emit == null) {
        if (wasm.llvm_object) |llvm_object| {
            return try llvm_object.flushModule(comp, prog_node);
        }
        return;
    }

    if (build_options.have_llvm and wasm.base.options.use_lld) {
        return wasm.linkWithLLD(comp, prog_node);
    } else if (wasm.base.options.use_llvm and !wasm.base.options.use_lld) {
        return wasm.linkWithZld(comp, prog_node);
    } else {
        return wasm.flushModule(comp, prog_node);
    }
}

/// Uses the in-house linker to link one or multiple object -and archive files into a WebAssembly binary.
fn linkWithZld(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) link.File.FlushError!void {
    const tracy = trace(@src());
    defer tracy.end();

    const gpa = wasm.base.allocator;
    const options = wasm.base.options;

    // Used for all temporary memory allocated during flushin
    var arena_instance = std.heap.ArenaAllocator.init(gpa);
    defer arena_instance.deinit();
    const arena = arena_instance.allocator();

    const directory = options.emit.?.directory; // Just an alias to make it shorter to type.
    const full_out_path = try directory.join(arena, &[_][]const u8{options.emit.?.sub_path});

    // If there is no Zig code to compile, then we should skip flushing the output file because it
    // will not be part of the linker line anyway.
    const module_obj_path: ?[]const u8 = if (options.module != null) blk: {
        assert(options.use_llvm); // `linkWithZld` should never be called when the Wasm backend is used
        try wasm.flushModule(comp, prog_node);

        if (fs.path.dirname(full_out_path)) |dirname| {
            break :blk try fs.path.join(arena, &.{ dirname, wasm.base.intermediary_basename.? });
        } else {
            break :blk wasm.base.intermediary_basename.?;
        }
    } else null;

    var sub_prog_node = prog_node.start("Wasm Flush", 0);
    sub_prog_node.activate();
    defer sub_prog_node.end();

    const is_obj = options.output_mode == .Obj;
    const compiler_rt_path: ?[]const u8 = if (options.include_compiler_rt and !is_obj)
        comp.compiler_rt_lib.?.full_object_path
    else
        null;
    const id_symlink_basename = "zld.id";

    var man: Cache.Manifest = undefined;
    defer if (!options.disable_lld_caching) man.deinit();
    var digest: [Cache.hex_digest_len]u8 = undefined;

    // NOTE: The following section must be maintained to be equal
    // as the section defined in `linkWithLLD`
    if (!options.disable_lld_caching) {
        man = comp.cache_parent.obtain();

        // We are about to obtain this lock, so here we give other processes a chance first.
        wasm.base.releaseLock();

        comptime assert(Compilation.link_hash_implementation_version == 10);

        for (options.objects) |obj| {
            _ = try man.addFile(obj.path, null);
            man.hash.add(obj.must_link);
        }
        for (comp.c_object_table.keys()) |key| {
            _ = try man.addFile(key.status.success.object_path, null);
        }
        try man.addOptionalFile(module_obj_path);
        try man.addOptionalFile(compiler_rt_path);
        man.hash.addOptionalBytes(options.entry);
        man.hash.addOptional(options.stack_size_override);
        man.hash.add(wasm.base.options.build_id);
        man.hash.add(options.import_memory);
        man.hash.add(options.import_table);
        man.hash.add(options.export_table);
        man.hash.addOptional(options.initial_memory);
        man.hash.addOptional(options.max_memory);
        man.hash.add(options.shared_memory);
        man.hash.addOptional(options.global_base);
        man.hash.add(options.export_symbol_names.len);
        // strip does not need to go into the linker hash because it is part of the hash namespace
        for (options.export_symbol_names) |symbol_name| {
            man.hash.addBytes(symbol_name);
        }

        // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
        _ = try man.hit();
        digest = man.final();

        var prev_digest_buf: [digest.len]u8 = undefined;
        const prev_digest: []u8 = Cache.readSmallFile(
            directory.handle,
            id_symlink_basename,
            &prev_digest_buf,
        ) catch |err| blk: {
            log.debug("WASM LLD new_digest={s} error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
            // Handle this as a cache miss.
            break :blk prev_digest_buf[0..0];
        };
        if (mem.eql(u8, prev_digest, &digest)) {
            log.debug("WASM LLD digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
            // Hot diggity dog! The output binary is already there.
            wasm.base.lock = man.toOwnedLock();
            return;
        }
        log.debug("WASM LLD prev_digest={s} new_digest={s}", .{ std.fmt.fmtSliceHexLower(prev_digest), std.fmt.fmtSliceHexLower(&digest) });

        // We are about to change the output file to be different, so we invalidate the build hash now.
        directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
            error.FileNotFound => {},
            else => |e| return e,
        };
    }

    // Positional arguments to the linker such as object files and static archives.
    var positionals = std.ArrayList([]const u8).init(arena);
    try positionals.ensureUnusedCapacity(options.objects.len);

    // When the target os is WASI, we allow linking with WASI-LIBC
    if (options.target.os.tag == .wasi) {
        const is_exe_or_dyn_lib = wasm.base.options.output_mode == .Exe or
            (wasm.base.options.output_mode == .Lib and wasm.base.options.link_mode == .Dynamic);
        if (is_exe_or_dyn_lib) {
            const wasi_emulated_libs = wasm.base.options.wasi_emulated_libs;
            for (wasi_emulated_libs) |crt_file| {
                try positionals.append(try comp.get_libc_crt_file(
                    arena,
                    wasi_libc.emulatedLibCRFileLibName(crt_file),
                ));
            }

            if (wasm.base.options.link_libc) {
                try positionals.append(try comp.get_libc_crt_file(
                    arena,
                    wasi_libc.execModelCrtFileFullName(wasm.base.options.wasi_exec_model),
                ));
                try positionals.append(try comp.get_libc_crt_file(arena, "libc.a"));
            }

            if (wasm.base.options.link_libcpp) {
                try positionals.append(comp.libcxx_static_lib.?.full_object_path);
                try positionals.append(comp.libcxxabi_static_lib.?.full_object_path);
            }
        }
    }

    if (module_obj_path) |path| {
        try positionals.append(path);
    }

    for (options.objects) |object| {
        try positionals.append(object.path);
    }

    for (comp.c_object_table.keys()) |c_object| {
        try positionals.append(c_object.status.success.object_path);
    }

    if (comp.compiler_rt_lib) |lib| {
        try positionals.append(lib.full_object_path);
    }

    try wasm.parseInputFiles(positionals.items);

    for (wasm.objects.items, 0..) |_, object_index| {
        try wasm.resolveSymbolsInObject(@as(u16, @intCast(object_index)));
    }

    var emit_features_count: u32 = 0;
    var enabled_features: [@typeInfo(types.Feature.Tag).Enum.fields.len]bool = undefined;
    try wasm.validateFeatures(&enabled_features, &emit_features_count);
    try wasm.resolveSymbolsInArchives();
    try wasm.resolveLazySymbols();
    try wasm.checkUndefinedSymbols();

    try wasm.setupInitFunctions();
    try wasm.setupStart();
    try wasm.setupImports();

    for (wasm.objects.items, 0..) |*object, object_index| {
        try object.parseIntoAtoms(gpa, @as(u16, @intCast(object_index)), wasm);
    }

    try wasm.allocateAtoms();
    try wasm.setupMemory();
    wasm.allocateVirtualAddresses();
    wasm.mapFunctionTable();
    try wasm.mergeSections();
    try wasm.mergeTypes();
    try wasm.initializeCallCtorsFunction();
    try wasm.setupInitMemoryFunction();
    try wasm.setupTLSRelocationsFunction();
    try wasm.initializeTLSFunction();
    try wasm.setupStartSection();
    try wasm.setupExports();
    try wasm.writeToFile(enabled_features, emit_features_count, arena);

    if (!wasm.base.options.disable_lld_caching) {
        // Update the file with the digest. If it fails we can continue; it only
        // means that the next invocation will have an unnecessary cache miss.
        Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
            log.warn("failed to save linking hash digest symlink: {s}", .{@errorName(err)});
        };
        // Again failure here only means an unnecessary cache miss.
        man.writeManifest() catch |err| {
            log.warn("failed to write cache manifest when linking: {s}", .{@errorName(err)});
        };
        // We hang on to this lock so that the output file path can be used without
        // other processes clobbering it.
        wasm.base.lock = man.toOwnedLock();
    }
}

pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) link.File.FlushError!void {
    const tracy = trace(@src());
    defer tracy.end();

    if (wasm.llvm_object) |llvm_object| {
        return try llvm_object.flushModule(comp, prog_node);
    }

    var sub_prog_node = prog_node.start("Wasm Flush", 0);
    sub_prog_node.activate();
    defer sub_prog_node.end();

    // ensure the error names table is populated when an error name is referenced
    try wasm.populateErrorNameTable();

    // Used for all temporary memory allocated during flushin
    var arena_instance = std.heap.ArenaAllocator.init(wasm.base.allocator);
    defer arena_instance.deinit();
    const arena = arena_instance.allocator();

    // Positional arguments to the linker such as object files and static archives.
    var positionals = std.ArrayList([]const u8).init(arena);
    try positionals.ensureUnusedCapacity(wasm.base.options.objects.len);

    for (wasm.base.options.objects) |object| {
        positionals.appendAssumeCapacity(object.path);
    }

    for (comp.c_object_table.keys()) |c_object| {
        try positionals.append(c_object.status.success.object_path);
    }

    if (comp.compiler_rt_lib) |lib| {
        try positionals.append(lib.full_object_path);
    }

    try wasm.parseInputFiles(positionals.items);

    for (wasm.objects.items, 0..) |_, object_index| {
        try wasm.resolveSymbolsInObject(@as(u16, @intCast(object_index)));
    }

    var emit_features_count: u32 = 0;
    var enabled_features: [@typeInfo(types.Feature.Tag).Enum.fields.len]bool = undefined;
    try wasm.validateFeatures(&enabled_features, &emit_features_count);
    try wasm.resolveSymbolsInArchives();
    try wasm.resolveLazySymbols();
    try wasm.checkUndefinedSymbols();

    // When we finish/error we reset the state of the linker
    // So we can rebuild the binary file on each incremental update
    defer wasm.resetState();
    try wasm.setupInitFunctions();
    try wasm.setupErrorsLen();
    try wasm.setupStart();
    try wasm.setupImports();
    if (wasm.base.options.module) |mod| {
        var decl_it = wasm.decls.iterator();
        while (decl_it.next()) |entry| {
            const decl = mod.declPtr(entry.key_ptr.*);
            if (decl.isExtern(mod)) continue;
            const atom_index = entry.value_ptr.*;
            const atom = wasm.getAtomPtr(atom_index);
            if (decl.ty.zigTypeTag(mod) == .Fn) {
                try wasm.parseAtom(atom_index, .function);
            } else if (decl.getOwnedVariable(mod)) |variable| {
                if (variable.is_const) {
                    try wasm.parseAtom(atom_index, .{ .data = .read_only });
                } else if (variable.init.toValue().isUndefDeep(mod)) {
                    // for safe build modes, we store the atom in the data segment,
                    // whereas for unsafe build modes we store it in bss.
                    const is_initialized = wasm.base.options.optimize_mode == .Debug or
                        wasm.base.options.optimize_mode == .ReleaseSafe;
                    try wasm.parseAtom(atom_index, .{ .data = if (is_initialized) .initialized else .uninitialized });
                } else {
                    // when the decl is all zeroes, we store the atom in the bss segment,
                    // in all other cases it will be in the data segment.
                    const is_zeroes = for (atom.code.items) |byte| {
                        if (byte != 0) break false;
                    } else true;
                    try wasm.parseAtom(atom_index, .{ .data = if (is_zeroes) .uninitialized else .initialized });
                }
            } else {
                try wasm.parseAtom(atom_index, .{ .data = .read_only });
            }

            // also parse atoms for a decl's locals
            for (atom.locals.items) |local_atom_index| {
                try wasm.parseAtom(local_atom_index, .{ .data = .read_only });
            }
        }

        // also parse any backend-generated functions
        for (wasm.synthetic_functions.items) |atom_index| {
            try wasm.parseAtom(atom_index, .function);
        }

        if (wasm.dwarf) |*dwarf| {
            try dwarf.flushModule(wasm.base.options.module.?);
        }
    }

    for (wasm.objects.items, 0..) |*object, object_index| {
        try object.parseIntoAtoms(wasm.base.allocator, @as(u16, @intCast(object_index)), wasm);
    }

    try wasm.allocateAtoms();
    try wasm.setupMemory();
    wasm.allocateVirtualAddresses();
    wasm.mapFunctionTable();
    try wasm.mergeSections();
    try wasm.mergeTypes();
    try wasm.initializeCallCtorsFunction();
    try wasm.setupInitMemoryFunction();
    try wasm.setupTLSRelocationsFunction();
    try wasm.initializeTLSFunction();
    try wasm.setupStartSection();
    try wasm.setupExports();
    try wasm.writeToFile(enabled_features, emit_features_count, arena);
}

/// Writes the WebAssembly in-memory module to the file
fn writeToFile(
    wasm: *Wasm,
    enabled_features: [@typeInfo(types.Feature.Tag).Enum.fields.len]bool,
    feature_count: u32,
    arena: Allocator,
) !void {
    // Size of each section header
    const header_size = 5 + 1;
    // The amount of sections that will be written
    var section_count: u32 = 0;
    // Index of the code section. Used to tell relocation table where the section lives.
    var code_section_index: ?u32 = null;
    // Index of the data section. Used to tell relocation table where the section lives.
    var data_section_index: ?u32 = null;
    const is_obj = wasm.base.options.output_mode == .Obj or (!wasm.base.options.use_llvm and wasm.base.options.use_lld);

    var binary_bytes = std.ArrayList(u8).init(wasm.base.allocator);
    defer binary_bytes.deinit();
    const binary_writer = binary_bytes.writer();

    // We write the magic bytes at the end so they will only be written
    // if everything succeeded as expected. So populate with 0's for now.
    try binary_writer.writeAll(&[_]u8{0} ** 8);
    // (Re)set file pointer to 0
    try wasm.base.file.?.setEndPos(0);
    try wasm.base.file.?.seekTo(0);

    // Type section
    if (wasm.func_types.items.len != 0) {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);
        log.debug("Writing type section. Count: ({d})", .{wasm.func_types.items.len});
        for (wasm.func_types.items) |func_type| {
            try leb.writeULEB128(binary_writer, std.wasm.function_type);
            try leb.writeULEB128(binary_writer, @as(u32, @intCast(func_type.params.len)));
            for (func_type.params) |param_ty| {
                try leb.writeULEB128(binary_writer, std.wasm.valtype(param_ty));
            }
            try leb.writeULEB128(binary_writer, @as(u32, @intCast(func_type.returns.len)));
            for (func_type.returns) |ret_ty| {
                try leb.writeULEB128(binary_writer, std.wasm.valtype(ret_ty));
            }
        }

        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .type,
            @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size)),
            @as(u32, @intCast(wasm.func_types.items.len)),
        );
        section_count += 1;
    }

    // Import section
    const import_memory = wasm.base.options.import_memory or is_obj;
    const export_memory = wasm.base.options.export_memory;
    if (wasm.imports.count() != 0 or import_memory) {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);

        var it = wasm.imports.iterator();
        while (it.next()) |entry| {
            assert(entry.key_ptr.*.getSymbol(wasm).isUndefined());
            const import = entry.value_ptr.*;
            try wasm.emitImport(binary_writer, import);
        }

        if (import_memory) {
            const mem_name = if (is_obj) "__linear_memory" else "memory";
            const mem_imp: types.Import = .{
                .module_name = try wasm.string_table.put(wasm.base.allocator, wasm.host_name),
                .name = try wasm.string_table.put(wasm.base.allocator, mem_name),
                .kind = .{ .memory = wasm.memories.limits },
            };
            try wasm.emitImport(binary_writer, mem_imp);
        }

        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .import,
            @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size)),
            @as(u32, @intCast(wasm.imports.count() + @intFromBool(import_memory))),
        );
        section_count += 1;
    }

    // Function section
    if (wasm.functions.count() != 0) {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);
        for (wasm.functions.values()) |function| {
            try leb.writeULEB128(binary_writer, function.type_index);
        }

        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .function,
            @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size)),
            @as(u32, @intCast(wasm.functions.count())),
        );
        section_count += 1;
    }

    // Table section
    if (wasm.tables.items.len > 0) {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);

        for (wasm.tables.items) |table| {
            try leb.writeULEB128(binary_writer, std.wasm.reftype(table.reftype));
            try emitLimits(binary_writer, table.limits);
        }

        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .table,
            @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size)),
            @as(u32, @intCast(wasm.tables.items.len)),
        );
        section_count += 1;
    }

    // Memory section
    if (!import_memory) {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);

        try emitLimits(binary_writer, wasm.memories.limits);
        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .memory,
            @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size)),
            @as(u32, 1), // wasm currently only supports 1 linear memory segment
        );
        section_count += 1;
    }

    // Global section (used to emit stack pointer)
    if (wasm.wasm_globals.items.len > 0) {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);

        for (wasm.wasm_globals.items) |global| {
            try binary_writer.writeByte(std.wasm.valtype(global.global_type.valtype));
            try binary_writer.writeByte(@intFromBool(global.global_type.mutable));
            try emitInit(binary_writer, global.init);
        }

        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .global,
            @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size)),
            @as(u32, @intCast(wasm.wasm_globals.items.len)),
        );
        section_count += 1;
    }

    // Export section
    if (wasm.exports.items.len != 0 or export_memory) {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);

        for (wasm.exports.items) |exp| {
            const name = wasm.string_table.get(exp.name);
            try leb.writeULEB128(binary_writer, @as(u32, @intCast(name.len)));
            try binary_writer.writeAll(name);
            try leb.writeULEB128(binary_writer, @intFromEnum(exp.kind));
            try leb.writeULEB128(binary_writer, exp.index);
        }

        if (export_memory) {
            try leb.writeULEB128(binary_writer, @as(u32, @intCast("memory".len)));
            try binary_writer.writeAll("memory");
            try binary_writer.writeByte(std.wasm.externalKind(.memory));
            try leb.writeULEB128(binary_writer, @as(u32, 0));
        }

        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .@"export",
            @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size)),
            @as(u32, @intCast(wasm.exports.items.len)) + @intFromBool(export_memory),
        );
        section_count += 1;
    }

    if (wasm.entry) |entry_index| {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);
        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .start,
            @intCast(binary_bytes.items.len - header_offset - header_size),
            entry_index,
        );
    }

    // element section (function table)
    if (wasm.function_table.count() > 0) {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);

        const table_loc = wasm.findGlobalSymbol("__indirect_function_table").?;
        const table_sym = table_loc.getSymbol(wasm);

        var flags: u32 = if (table_sym.index == 0) 0x0 else 0x02; // passive with implicit 0-index table or set table index manually
        try leb.writeULEB128(binary_writer, flags);
        if (flags == 0x02) {
            try leb.writeULEB128(binary_writer, table_sym.index);
        }
        try emitInit(binary_writer, .{ .i32_const = 1 }); // We start at index 1, so unresolved function pointers are invalid
        if (flags == 0x02) {
            try leb.writeULEB128(binary_writer, @as(u8, 0)); // represents funcref
        }
        try leb.writeULEB128(binary_writer, @as(u32, @intCast(wasm.function_table.count())));
        var symbol_it = wasm.function_table.keyIterator();
        while (symbol_it.next()) |symbol_loc_ptr| {
            try leb.writeULEB128(binary_writer, symbol_loc_ptr.*.getSymbol(wasm).index);
        }

        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .element,
            @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size)),
            @as(u32, 1),
        );
        section_count += 1;
    }

    // When the shared-memory option is enabled, we *must* emit the 'data count' section.
    const data_segments_count = wasm.data_segments.count() - @intFromBool(wasm.data_segments.contains(".bss") and !import_memory);
    if (data_segments_count != 0 and wasm.base.options.shared_memory) {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);
        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .data_count,
            @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size)),
            @as(u32, @intCast(data_segments_count)),
        );
    }

    // Code section
    var code_section_size: u32 = 0;
    if (wasm.code_section_index) |code_index| {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);
        var atom_index = wasm.atoms.get(code_index).?;

        // The code section must be sorted in line with the function order.
        var sorted_atoms = try std.ArrayList(*Atom).initCapacity(wasm.base.allocator, wasm.functions.count());
        defer sorted_atoms.deinit();

        while (true) {
            var atom = wasm.getAtomPtr(atom_index);
            if (wasm.resolved_symbols.contains(atom.symbolLoc())) {
                if (!is_obj) {
                    atom.resolveRelocs(wasm);
                }
                sorted_atoms.appendAssumeCapacity(atom);
            }
            // atom = if (atom.prev) |prev| wasm.getAtomPtr(prev) else break;
            atom_index = atom.prev orelse break;
        }

        const atom_sort_fn = struct {
            fn sort(ctx: *const Wasm, lhs: *const Atom, rhs: *const Atom) bool {
                const lhs_sym = lhs.symbolLoc().getSymbol(ctx);
                const rhs_sym = rhs.symbolLoc().getSymbol(ctx);
                return lhs_sym.index < rhs_sym.index;
            }
        }.sort;

        mem.sort(*Atom, sorted_atoms.items, wasm, atom_sort_fn);

        for (sorted_atoms.items) |sorted_atom| {
            try leb.writeULEB128(binary_writer, sorted_atom.size);
            try binary_writer.writeAll(sorted_atom.code.items);
        }

        code_section_size = @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size));
        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .code,
            code_section_size,
            @as(u32, @intCast(wasm.functions.count())),
        );
        code_section_index = section_count;
        section_count += 1;
    }

    // Data section
    if (data_segments_count != 0) {
        const header_offset = try reserveVecSectionHeader(&binary_bytes);

        var it = wasm.data_segments.iterator();
        var segment_count: u32 = 0;
        while (it.next()) |entry| {
            // do not output 'bss' section unless we import memory and therefore
            // want to guarantee the data is zero initialized
            if (!import_memory and std.mem.eql(u8, entry.key_ptr.*, ".bss")) continue;
            const segment_index = entry.value_ptr.*;
            const segment = wasm.segments.items[segment_index];
            if (segment.size == 0) continue; // do not emit empty segments
            segment_count += 1;
            var atom_index = wasm.atoms.get(segment_index).?;

            try leb.writeULEB128(binary_writer, segment.flags);
            if (segment.flags & @intFromEnum(Wasm.Segment.Flag.WASM_DATA_SEGMENT_HAS_MEMINDEX) != 0) {
                try leb.writeULEB128(binary_writer, @as(u32, 0)); // memory is always index 0 as we only have 1 memory entry
            }
            // when a segment is passive, it's initialized during runtime.
            if (!segment.isPassive()) {
                try emitInit(binary_writer, .{ .i32_const = @as(i32, @bitCast(segment.offset)) });
            }
            // offset into data section
            try leb.writeULEB128(binary_writer, segment.size);

            // fill in the offset table and the data segments
            var current_offset: u32 = 0;
            while (true) {
                const atom = wasm.getAtomPtr(atom_index);
                if (!is_obj) {
                    atom.resolveRelocs(wasm);
                }

                // Pad with zeroes to ensure all segments are aligned
                if (current_offset != atom.offset) {
                    const diff = atom.offset - current_offset;
                    try binary_writer.writeByteNTimes(0, diff);
                    current_offset += diff;
                }
                assert(current_offset == atom.offset);
                assert(atom.code.items.len == atom.size);
                try binary_writer.writeAll(atom.code.items);

                current_offset += atom.size;
                if (atom.prev) |prev| {
                    atom_index = prev;
                } else {
                    // also pad with zeroes when last atom to ensure
                    // segments are aligned.
                    if (current_offset != segment.size) {
                        try binary_writer.writeByteNTimes(0, segment.size - current_offset);
                        current_offset += segment.size - current_offset;
                    }
                    break;
                }
            }
            assert(current_offset == segment.size);
        }

        try writeVecSectionHeader(
            binary_bytes.items,
            header_offset,
            .data,
            @as(u32, @intCast(binary_bytes.items.len - header_offset - header_size)),
            @as(u32, @intCast(segment_count)),
        );
        data_section_index = section_count;
        section_count += 1;
    }

    if (is_obj) {
        // relocations need to point to the index of a symbol in the final symbol table. To save memory,
        // we never store all symbols in a single table, but store a location reference instead.
        // This means that for a relocatable object file, we need to generate one and provide it to the relocation sections.
        var symbol_table = std.AutoArrayHashMap(SymbolLoc, u32).init(arena);
        try wasm.emitLinkSection(&binary_bytes, &symbol_table);
        if (code_section_index) |code_index| {
            try wasm.emitCodeRelocations(&binary_bytes, code_index, symbol_table);
        }
        if (data_section_index) |data_index| {
            try wasm.emitDataRelocations(&binary_bytes, data_index, symbol_table);
        }
    } else if (!wasm.base.options.strip) {
        try wasm.emitNameSection(&binary_bytes, arena);
    }

    if (!wasm.base.options.strip) {
        // The build id must be computed on the main sections only,
        // so we have to do it now, before the debug sections.
        switch (wasm.base.options.build_id) {
            .none => {},
            .fast => {
                var id: [16]u8 = undefined;
                std.crypto.hash.sha3.TurboShake128(null).hash(binary_bytes.items, &id, .{});
                var uuid: [36]u8 = undefined;
                _ = try std.fmt.bufPrint(&uuid, "{s}-{s}-{s}-{s}-{s}", .{
                    std.fmt.fmtSliceHexLower(id[0..4]),
                    std.fmt.fmtSliceHexLower(id[4..6]),
                    std.fmt.fmtSliceHexLower(id[6..8]),
                    std.fmt.fmtSliceHexLower(id[8..10]),
                    std.fmt.fmtSliceHexLower(id[10..]),
                });
                try emitBuildIdSection(&binary_bytes, &uuid);
            },
            .hexstring => |hs| {
                var buffer: [32 * 2]u8 = undefined;
                const str = std.fmt.bufPrint(&buffer, "{s}", .{
                    std.fmt.fmtSliceHexLower(hs.toSlice()),
                }) catch unreachable;
                try emitBuildIdSection(&binary_bytes, str);
            },
            else => |mode| log.err("build-id '{s}' is not supported for WASM", .{@tagName(mode)}),
        }

        // if (wasm.dwarf) |*dwarf| {
        //     const mod = wasm.base.options.module.?;
        //     try dwarf.writeDbgAbbrev();
        //     // for debug info and ranges, the address is always 0,
        //     // as locations are always offsets relative to 'code' section.
        //     try dwarf.writeDbgInfoHeader(mod, 0, code_section_size);
        //     try dwarf.writeDbgAranges(0, code_section_size);
        //     try dwarf.writeDbgLineHeader();
        // }

        var debug_bytes = std.ArrayList(u8).init(wasm.base.allocator);
        defer debug_bytes.deinit();

        const DebugSection = struct {
            name: []const u8,
            index: ?u32,
        };

        const debug_sections: []const DebugSection = &.{
            .{ .name = ".debug_info", .index = wasm.debug_info_index },
            .{ .name = ".debug_pubtypes", .index = wasm.debug_pubtypes_index },
            .{ .name = ".debug_abbrev", .index = wasm.debug_abbrev_index },
            .{ .name = ".debug_line", .index = wasm.debug_line_index },
            .{ .name = ".debug_str", .index = wasm.debug_str_index },
            .{ .name = ".debug_pubnames", .index = wasm.debug_pubnames_index },
            .{ .name = ".debug_loc", .index = wasm.debug_loc_index },
            .{ .name = ".debug_ranges", .index = wasm.debug_ranges_index },
        };

        for (debug_sections) |item| {
            if (item.index) |index| {
                var atom = wasm.getAtomPtr(wasm.atoms.get(index).?);
                while (true) {
                    atom.resolveRelocs(wasm);
                    try debug_bytes.appendSlice(atom.code.items);
                    atom = if (atom.prev) |prev| wasm.getAtomPtr(prev) else break;
                }
                try emitDebugSection(&binary_bytes, debug_bytes.items, item.name);
                debug_bytes.clearRetainingCapacity();
            }
        }

        try emitProducerSection(&binary_bytes);
        if (feature_count > 0) {
            try emitFeaturesSection(&binary_bytes, &enabled_features, feature_count);
        }
    }

    // Only when writing all sections executed properly we write the magic
    // bytes. This allows us to easily detect what went wrong while generating
    // the final binary.
    {
        const src = std.wasm.magic ++ std.wasm.version;
        binary_bytes.items[0..src.len].* = src;
    }

    // finally, write the entire binary into the file.
    var iovec = [_]std.os.iovec_const{.{
        .iov_base = binary_bytes.items.ptr,
        .iov_len = binary_bytes.items.len,
    }};
    try wasm.base.file.?.writevAll(&iovec);
}

fn emitDebugSection(binary_bytes: *std.ArrayList(u8), data: []const u8, name: []const u8) !void {
    if (data.len == 0) return;
    const header_offset = try reserveCustomSectionHeader(binary_bytes);
    const writer = binary_bytes.writer();
    try leb.writeULEB128(writer, @as(u32, @intCast(name.len)));
    try writer.writeAll(name);

    const start = binary_bytes.items.len - header_offset;
    log.debug("Emit debug section: '{s}' start=0x{x:0>8} end=0x{x:0>8}", .{ name, start, start + data.len });
    try writer.writeAll(data);

    try writeCustomSectionHeader(
        binary_bytes.items,
        header_offset,
        @as(u32, @intCast(binary_bytes.items.len - header_offset - 6)),
    );
}

fn emitProducerSection(binary_bytes: *std.ArrayList(u8)) !void {
    const header_offset = try reserveCustomSectionHeader(binary_bytes);

    const writer = binary_bytes.writer();
    const producers = "producers";
    try leb.writeULEB128(writer, @as(u32, @intCast(producers.len)));
    try writer.writeAll(producers);

    try leb.writeULEB128(writer, @as(u32, 2)); // 2 fields: Language + processed-by

    // used for the Zig version
    var version_buf: [100]u8 = undefined;
    const version = try std.fmt.bufPrint(&version_buf, "{}", .{build_options.semver});

    // language field
    {
        const language = "language";
        try leb.writeULEB128(writer, @as(u32, @intCast(language.len)));
        try writer.writeAll(language);

        // field_value_count (TODO: Parse object files for producer sections to detect their language)
        try leb.writeULEB128(writer, @as(u32, 1));

        // versioned name
        {
            try leb.writeULEB128(writer, @as(u32, 3)); // len of "Zig"
            try writer.writeAll("Zig");

            try leb.writeULEB128(writer, @as(u32, @intCast(version.len)));
            try writer.writeAll(version);
        }
    }

    // processed-by field
    {
        const processed_by = "processed-by";
        try leb.writeULEB128(writer, @as(u32, @intCast(processed_by.len)));
        try writer.writeAll(processed_by);

        // field_value_count (TODO: Parse object files for producer sections to detect other used tools)
        try leb.writeULEB128(writer, @as(u32, 1));

        // versioned name
        {
            try leb.writeULEB128(writer, @as(u32, 3)); // len of "Zig"
            try writer.writeAll("Zig");

            try leb.writeULEB128(writer, @as(u32, @intCast(version.len)));
            try writer.writeAll(version);
        }
    }

    try writeCustomSectionHeader(
        binary_bytes.items,
        header_offset,
        @as(u32, @intCast(binary_bytes.items.len - header_offset - 6)),
    );
}

fn emitBuildIdSection(binary_bytes: *std.ArrayList(u8), build_id: []const u8) !void {
    const header_offset = try reserveCustomSectionHeader(binary_bytes);

    const writer = binary_bytes.writer();
    const hdr_build_id = "build_id";
    try leb.writeULEB128(writer, @as(u32, @intCast(hdr_build_id.len)));
    try writer.writeAll(hdr_build_id);

    try leb.writeULEB128(writer, @as(u32, 1));
    try leb.writeULEB128(writer, @as(u32, @intCast(build_id.len)));
    try writer.writeAll(build_id);

    try writeCustomSectionHeader(
        binary_bytes.items,
        header_offset,
        @as(u32, @intCast(binary_bytes.items.len - header_offset - 6)),
    );
}

fn emitFeaturesSection(binary_bytes: *std.ArrayList(u8), enabled_features: []const bool, features_count: u32) !void {
    const header_offset = try reserveCustomSectionHeader(binary_bytes);

    const writer = binary_bytes.writer();
    const target_features = "target_features";
    try leb.writeULEB128(writer, @as(u32, @intCast(target_features.len)));
    try writer.writeAll(target_features);

    try leb.writeULEB128(writer, features_count);
    for (enabled_features, 0..) |enabled, feature_index| {
        if (enabled) {
            const feature: types.Feature = .{ .prefix = .used, .tag = @as(types.Feature.Tag, @enumFromInt(feature_index)) };
            try leb.writeULEB128(writer, @intFromEnum(feature.prefix));
            var buf: [100]u8 = undefined;
            const string = try std.fmt.bufPrint(&buf, "{}", .{feature.tag});
            try leb.writeULEB128(writer, @as(u32, @intCast(string.len)));
            try writer.writeAll(string);
        }
    }

    try writeCustomSectionHeader(
        binary_bytes.items,
        header_offset,
        @as(u32, @intCast(binary_bytes.items.len - header_offset - 6)),
    );
}

fn emitNameSection(wasm: *Wasm, binary_bytes: *std.ArrayList(u8), arena: std.mem.Allocator) !void {
    const Name = struct {
        index: u32,
        name: []const u8,

        fn lessThan(context: void, lhs: @This(), rhs: @This()) bool {
            _ = context;
            return lhs.index < rhs.index;
        }
    };

    // we must de-duplicate symbols that point to the same function
    var funcs = std.AutoArrayHashMap(u32, Name).init(arena);
    try funcs.ensureUnusedCapacity(wasm.functions.count() + wasm.imported_functions_count);
    var globals = try std.ArrayList(Name).initCapacity(arena, wasm.wasm_globals.items.len + wasm.imported_globals_count);
    var segments = try std.ArrayList(Name).initCapacity(arena, wasm.data_segments.count());

    for (wasm.resolved_symbols.keys()) |sym_loc| {
        const symbol = sym_loc.getSymbol(wasm).*;
        const name = sym_loc.getName(wasm);
        switch (symbol.tag) {
            .function => {
                const gop = funcs.getOrPutAssumeCapacity(symbol.index);
                if (!gop.found_existing) {
                    gop.value_ptr.* = .{ .index = symbol.index, .name = name };
                }
            },
            .global => globals.appendAssumeCapacity(.{ .index = symbol.index, .name = name }),
            else => {},
        }
    }
    // data segments are already 'ordered'
    var data_segment_index: u32 = 0;
    for (wasm.data_segments.keys()) |key| {
        // bss section is not emitted when this condition holds true, so we also
        // do not output a name for it.
        if (!wasm.base.options.import_memory and std.mem.eql(u8, key, ".bss")) continue;
        segments.appendAssumeCapacity(.{ .index = data_segment_index, .name = key });
        data_segment_index += 1;
    }

    mem.sort(Name, funcs.values(), {}, Name.lessThan);
    mem.sort(Name, globals.items, {}, Name.lessThan);

    const header_offset = try reserveCustomSectionHeader(binary_bytes);
    const writer = binary_bytes.writer();
    try leb.writeULEB128(writer, @as(u32, @intCast("name".len)));
    try writer.writeAll("name");

    try wasm.emitNameSubsection(.function, funcs.values(), writer);
    try wasm.emitNameSubsection(.global, globals.items, writer);
    try wasm.emitNameSubsection(.data_segment, segments.items, writer);

    try writeCustomSectionHeader(
        binary_bytes.items,
        header_offset,
        @as(u32, @intCast(binary_bytes.items.len - header_offset - 6)),
    );
}

fn emitNameSubsection(wasm: *Wasm, section_id: std.wasm.NameSubsection, names: anytype, writer: anytype) !void {
    // We must emit subsection size, so first write to a temporary list
    var section_list = std.ArrayList(u8).init(wasm.base.allocator);
    defer section_list.deinit();
    const sub_writer = section_list.writer();

    try leb.writeULEB128(sub_writer, @as(u32, @intCast(names.len)));
    for (names) |name| {
        log.debug("Emit symbol '{s}' type({s})", .{ name.name, @tagName(section_id) });
        try leb.writeULEB128(sub_writer, name.index);
        try leb.writeULEB128(sub_writer, @as(u32, @intCast(name.name.len)));
        try sub_writer.writeAll(name.name);
    }

    // From now, write to the actual writer
    try leb.writeULEB128(writer, @intFromEnum(section_id));
    try leb.writeULEB128(writer, @as(u32, @intCast(section_list.items.len)));
    try writer.writeAll(section_list.items);
}

fn emitLimits(writer: anytype, limits: std.wasm.Limits) !void {
    try writer.writeByte(limits.flags);
    try leb.writeULEB128(writer, limits.min);
    if (limits.hasFlag(.WASM_LIMITS_FLAG_HAS_MAX)) {
        try leb.writeULEB128(writer, limits.max);
    }
}

fn emitInit(writer: anytype, init_expr: std.wasm.InitExpression) !void {
    switch (init_expr) {
        .i32_const => |val| {
            try writer.writeByte(std.wasm.opcode(.i32_const));
            try leb.writeILEB128(writer, val);
        },
        .i64_const => |val| {
            try writer.writeByte(std.wasm.opcode(.i64_const));
            try leb.writeILEB128(writer, val);
        },
        .f32_const => |val| {
            try writer.writeByte(std.wasm.opcode(.f32_const));
            try writer.writeIntLittle(u32, @as(u32, @bitCast(val)));
        },
        .f64_const => |val| {
            try writer.writeByte(std.wasm.opcode(.f64_const));
            try writer.writeIntLittle(u64, @as(u64, @bitCast(val)));
        },
        .global_get => |val| {
            try writer.writeByte(std.wasm.opcode(.global_get));
            try leb.writeULEB128(writer, val);
        },
    }
    try writer.writeByte(std.wasm.opcode(.end));
}

fn emitImport(wasm: *Wasm, writer: anytype, import: types.Import) !void {
    const module_name = wasm.string_table.get(import.module_name);
    try leb.writeULEB128(writer, @as(u32, @intCast(module_name.len)));
    try writer.writeAll(module_name);

    const name = wasm.string_table.get(import.name);
    try leb.writeULEB128(writer, @as(u32, @intCast(name.len)));
    try writer.writeAll(name);

    try writer.writeByte(@intFromEnum(import.kind));
    switch (import.kind) {
        .function => |type_index| try leb.writeULEB128(writer, type_index),
        .global => |global_type| {
            try leb.writeULEB128(writer, std.wasm.valtype(global_type.valtype));
            try writer.writeByte(@intFromBool(global_type.mutable));
        },
        .table => |table| {
            try leb.writeULEB128(writer, std.wasm.reftype(table.reftype));
            try emitLimits(writer, table.limits);
        },
        .memory => |limits| {
            try emitLimits(writer, limits);
        },
    }
}

fn linkWithLLD(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) !void {
    const tracy = trace(@src());
    defer tracy.end();

    var arena_allocator = std.heap.ArenaAllocator.init(wasm.base.allocator);
    defer arena_allocator.deinit();
    const arena = arena_allocator.allocator();

    const directory = wasm.base.options.emit.?.directory; // Just an alias to make it shorter to type.
    const full_out_path = try directory.join(arena, &[_][]const u8{wasm.base.options.emit.?.sub_path});

    // If there is no Zig code to compile, then we should skip flushing the output file because it
    // will not be part of the linker line anyway.
    const module_obj_path: ?[]const u8 = if (wasm.base.options.module != null) blk: {
        try wasm.flushModule(comp, prog_node);

        if (fs.path.dirname(full_out_path)) |dirname| {
            break :blk try fs.path.join(arena, &.{ dirname, wasm.base.intermediary_basename.? });
        } else {
            break :blk wasm.base.intermediary_basename.?;
        }
    } else null;

    var sub_prog_node = prog_node.start("LLD Link", 0);
    sub_prog_node.activate();
    sub_prog_node.context.refresh();
    defer sub_prog_node.end();

    const is_obj = wasm.base.options.output_mode == .Obj;

    const compiler_rt_path: ?[]const u8 = if (wasm.base.options.include_compiler_rt and !is_obj)
        comp.compiler_rt_lib.?.full_object_path
    else
        null;

    const target = wasm.base.options.target;

    const id_symlink_basename = "lld.id";

    var man: Cache.Manifest = undefined;
    defer if (!wasm.base.options.disable_lld_caching) man.deinit();

    var digest: [Cache.hex_digest_len]u8 = undefined;

    if (!wasm.base.options.disable_lld_caching) {
        man = comp.cache_parent.obtain();

        // We are about to obtain this lock, so here we give other processes a chance first.
        wasm.base.releaseLock();

        comptime assert(Compilation.link_hash_implementation_version == 10);

        for (wasm.base.options.objects) |obj| {
            _ = try man.addFile(obj.path, null);
            man.hash.add(obj.must_link);
        }
        for (comp.c_object_table.keys()) |key| {
            _ = try man.addFile(key.status.success.object_path, null);
        }
        try man.addOptionalFile(module_obj_path);
        try man.addOptionalFile(compiler_rt_path);
        man.hash.addOptionalBytes(wasm.base.options.entry);
        man.hash.addOptional(wasm.base.options.stack_size_override);
        man.hash.add(wasm.base.options.build_id);
        man.hash.add(wasm.base.options.import_memory);
        man.hash.add(wasm.base.options.export_memory);
        man.hash.add(wasm.base.options.import_table);
        man.hash.add(wasm.base.options.export_table);
        man.hash.addOptional(wasm.base.options.initial_memory);
        man.hash.addOptional(wasm.base.options.max_memory);
        man.hash.add(wasm.base.options.shared_memory);
        man.hash.addOptional(wasm.base.options.global_base);
        man.hash.add(wasm.base.options.export_symbol_names.len);
        // strip does not need to go into the linker hash because it is part of the hash namespace
        for (wasm.base.options.export_symbol_names) |symbol_name| {
            man.hash.addBytes(symbol_name);
        }

        // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
        _ = try man.hit();
        digest = man.final();

        var prev_digest_buf: [digest.len]u8 = undefined;
        const prev_digest: []u8 = Cache.readSmallFile(
            directory.handle,
            id_symlink_basename,
            &prev_digest_buf,
        ) catch |err| blk: {
            log.debug("WASM LLD new_digest={s} error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
            // Handle this as a cache miss.
            break :blk prev_digest_buf[0..0];
        };
        if (mem.eql(u8, prev_digest, &digest)) {
            log.debug("WASM LLD digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
            // Hot diggity dog! The output binary is already there.
            wasm.base.lock = man.toOwnedLock();
            return;
        }
        log.debug("WASM LLD prev_digest={s} new_digest={s}", .{ std.fmt.fmtSliceHexLower(prev_digest), std.fmt.fmtSliceHexLower(&digest) });

        // We are about to change the output file to be different, so we invalidate the build hash now.
        directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
            error.FileNotFound => {},
            else => |e| return e,
        };
    }

    if (is_obj) {
        // LLD's WASM driver does not support the equivalent of `-r` so we do a simple file copy
        // here. TODO: think carefully about how we can avoid this redundant operation when doing
        // build-obj. See also the corresponding TODO in linkAsArchive.
        const the_object_path = blk: {
            if (wasm.base.options.objects.len != 0)
                break :blk wasm.base.options.objects[0].path;

            if (comp.c_object_table.count() != 0)
                break :blk comp.c_object_table.keys()[0].status.success.object_path;

            if (module_obj_path) |p|
                break :blk p;

            // TODO I think this is unreachable. Audit this situation when solving the above TODO
            // regarding eliding redundant object -> object transformations.
            return error.NoObjectsToLink;
        };
        // This can happen when using --enable-cache and using the stage1 backend. In this case
        // we can skip the file copy.
        if (!mem.eql(u8, the_object_path, full_out_path)) {
            try fs.cwd().copyFile(the_object_path, fs.cwd(), full_out_path, .{});
        }
    } else {
        // Create an LLD command line and invoke it.
        var argv = std.ArrayList([]const u8).init(wasm.base.allocator);
        defer argv.deinit();
        // We will invoke ourselves as a child process to gain access to LLD.
        // This is necessary because LLD does not behave properly as a library -
        // it calls exit() and does not reset all global data between invocations.
        const linker_command = "wasm-ld";
        try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, linker_command });
        try argv.append("--error-limit=0");

        if (wasm.base.options.lto) {
            switch (wasm.base.options.optimize_mode) {
                .Debug => {},
                .ReleaseSmall => try argv.append("-O2"),
                .ReleaseFast, .ReleaseSafe => try argv.append("-O3"),
            }
        }

        if (wasm.base.options.import_memory) {
            try argv.append("--import-memory");
        }

        if (wasm.base.options.export_memory) {
            try argv.append("--export-memory");
        }

        if (wasm.base.options.import_table) {
            assert(!wasm.base.options.export_table);
            try argv.append("--import-table");
        }

        if (wasm.base.options.export_table) {
            assert(!wasm.base.options.import_table);
            try argv.append("--export-table");
        }

        if (wasm.base.options.strip) {
            try argv.append("-s");
        }

        if (wasm.base.options.initial_memory) |initial_memory| {
            const arg = try std.fmt.allocPrint(arena, "--initial-memory={d}", .{initial_memory});
            try argv.append(arg);
        }

        if (wasm.base.options.max_memory) |max_memory| {
            const arg = try std.fmt.allocPrint(arena, "--max-memory={d}", .{max_memory});
            try argv.append(arg);
        }

        if (wasm.base.options.shared_memory) {
            try argv.append("--shared-memory");
        }

        if (wasm.base.options.global_base) |global_base| {
            const arg = try std.fmt.allocPrint(arena, "--global-base={d}", .{global_base});
            try argv.append(arg);
        } else {
            // We prepend it by default, so when a stack overflow happens the runtime will trap correctly,
            // rather than silently overwrite all global declarations. See https://github.com/ziglang/zig/issues/4496
            //
            // The user can overwrite this behavior by setting the global-base
            try argv.append("--stack-first");
        }

        // Users are allowed to specify which symbols they want to export to the wasm host.
        for (wasm.base.options.export_symbol_names) |symbol_name| {
            const arg = try std.fmt.allocPrint(arena, "--export={s}", .{symbol_name});
            try argv.append(arg);
        }

        if (wasm.base.options.rdynamic) {
            try argv.append("--export-dynamic");
        }

        if (wasm.base.options.entry) |entry| {
            try argv.append("--entry");
            try argv.append(entry);
        }

        // Increase the default stack size to a more reasonable value of 1MB instead of
        // the default of 1 Wasm page being 64KB, unless overridden by the user.
        try argv.append("-z");
        const stack_size = wasm.base.options.stack_size_override orelse std.wasm.page_size * 16;
        const arg = try std.fmt.allocPrint(arena, "stack-size={d}", .{stack_size});
        try argv.append(arg);

        if (wasm.base.options.output_mode == .Exe) {
            if (wasm.base.options.wasi_exec_model == .reactor) {
                // Reactor execution model does not have _start so lld doesn't look for it.
                try argv.append("--no-entry");
                // Make sure "_initialize" and other used-defined functions are exported if this is WASI reactor.
                // If rdynamic is true, it will already be appended, so only verify if the user did not specify
                // the flag in which case, we ensure `--export-dynamic` is called.
                if (!wasm.base.options.rdynamic) {
                    try argv.append("--export-dynamic");
                }
            }
        } else if (wasm.base.options.entry == null) {
            try argv.append("--no-entry"); // So lld doesn't look for _start.
        }
        if (wasm.base.options.import_symbols) {
            try argv.append("--allow-undefined");
        }

        // XXX - TODO: add when wasm-ld supports --build-id.
        // if (wasm.base.options.build_id) {
        //     try argv.append("--build-id=tree");
        // }

        try argv.appendSlice(&.{ "-o", full_out_path });

        if (target.cpu.arch == .wasm64) {
            try argv.append("-mwasm64");
        }

        if (target.os.tag == .wasi) {
            const is_exe_or_dyn_lib = wasm.base.options.output_mode == .Exe or
                (wasm.base.options.output_mode == .Lib and wasm.base.options.link_mode == .Dynamic);
            if (is_exe_or_dyn_lib) {
                const wasi_emulated_libs = wasm.base.options.wasi_emulated_libs;
                for (wasi_emulated_libs) |crt_file| {
                    try argv.append(try comp.get_libc_crt_file(
                        arena,
                        wasi_libc.emulatedLibCRFileLibName(crt_file),
                    ));
                }

                if (wasm.base.options.link_libc) {
                    try argv.append(try comp.get_libc_crt_file(
                        arena,
                        wasi_libc.execModelCrtFileFullName(wasm.base.options.wasi_exec_model),
                    ));
                    try argv.append(try comp.get_libc_crt_file(arena, "libc.a"));
                }

                if (wasm.base.options.link_libcpp) {
                    try argv.append(comp.libcxx_static_lib.?.full_object_path);
                    try argv.append(comp.libcxxabi_static_lib.?.full_object_path);
                }
            }
        }

        // Positional arguments to the linker such as object files.
        var whole_archive = false;
        for (wasm.base.options.objects) |obj| {
            if (obj.must_link and !whole_archive) {
                try argv.append("-whole-archive");
                whole_archive = true;
            } else if (!obj.must_link and whole_archive) {
                try argv.append("-no-whole-archive");
                whole_archive = false;
            }
            try argv.append(obj.path);
        }
        if (whole_archive) {
            try argv.append("-no-whole-archive");
            whole_archive = false;
        }

        for (comp.c_object_table.keys()) |key| {
            try argv.append(key.status.success.object_path);
        }
        if (module_obj_path) |p| {
            try argv.append(p);
        }

        if (wasm.base.options.output_mode != .Obj and
            !wasm.base.options.skip_linker_dependencies and
            !wasm.base.options.link_libc)
        {
            try argv.append(comp.libc_static_lib.?.full_object_path);
        }

        if (compiler_rt_path) |p| {
            try argv.append(p);
        }

        if (wasm.base.options.verbose_link) {
            // Skip over our own name so that the LLD linker name is the first argv item.
            Compilation.dump_argv(argv.items[1..]);
        }

        if (std.process.can_spawn) {
            // If possible, we run LLD as a child process because it does not always
            // behave properly as a library, unfortunately.
            // https://github.com/ziglang/zig/issues/3825
            var child = std.ChildProcess.init(argv.items, arena);
            if (comp.clang_passthrough_mode) {
                child.stdin_behavior = .Inherit;
                child.stdout_behavior = .Inherit;
                child.stderr_behavior = .Inherit;

                const term = child.spawnAndWait() catch |err| {
                    log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
                    return error.UnableToSpawnWasm;
                };
                switch (term) {
                    .Exited => |code| {
                        if (code != 0) {
                            std.process.exit(code);
                        }
                    },
                    else => std.process.abort(),
                }
            } else {
                child.stdin_behavior = .Ignore;
                child.stdout_behavior = .Ignore;
                child.stderr_behavior = .Pipe;

                try child.spawn();

                const stderr = try child.stderr.?.reader().readAllAlloc(arena, std.math.maxInt(usize));

                const term = child.wait() catch |err| {
                    log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
                    return error.UnableToSpawnWasm;
                };

                switch (term) {
                    .Exited => |code| {
                        if (code != 0) {
                            comp.lockAndParseLldStderr(linker_command, stderr);
                            return error.LLDReportedFailure;
                        }
                    },
                    else => {
                        log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
                        return error.LLDCrashed;
                    },
                }

                if (stderr.len != 0) {
                    log.warn("unexpected LLD stderr:\n{s}", .{stderr});
                }
            }
        } else {
            const exit_code = try lldMain(arena, argv.items, false);
            if (exit_code != 0) {
                if (comp.clang_passthrough_mode) {
                    std.process.exit(exit_code);
                } else {
                    return error.LLDReportedFailure;
                }
            }
        }

        // Give +x to the .wasm file if it is an executable and the OS is WASI.
        // Some systems may be configured to execute such binaries directly. Even if that
        // is not the case, it means we will get "exec format error" when trying to run
        // it, and then can react to that in the same way as trying to run an ELF file
        // from a foreign CPU architecture.
        if (fs.has_executable_bit and target.os.tag == .wasi and
            wasm.base.options.output_mode == .Exe)
        {
            // TODO: what's our strategy for reporting linker errors from this function?
            // report a nice error here with the file path if it fails instead of
            // just returning the error code.
            // chmod does not interact with umask, so we use a conservative -rwxr--r-- here.
            try std.os.fchmodat(fs.cwd().fd, full_out_path, 0o744, 0);
        }
    }

    if (!wasm.base.options.disable_lld_caching) {
        // Update the file with the digest. If it fails we can continue; it only
        // means that the next invocation will have an unnecessary cache miss.
        Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
            log.warn("failed to save linking hash digest symlink: {s}", .{@errorName(err)});
        };
        // Again failure here only means an unnecessary cache miss.
        man.writeManifest() catch |err| {
            log.warn("failed to write cache manifest when linking: {s}", .{@errorName(err)});
        };
        // We hang on to this lock so that the output file path can be used without
        // other processes clobbering it.
        wasm.base.lock = man.toOwnedLock();
    }
}

fn reserveVecSectionHeader(bytes: *std.ArrayList(u8)) !u32 {
    // section id + fixed leb contents size + fixed leb vector length
    const header_size = 1 + 5 + 5;
    const offset = @as(u32, @intCast(bytes.items.len));
    try bytes.appendSlice(&[_]u8{0} ** header_size);
    return offset;
}

fn reserveCustomSectionHeader(bytes: *std.ArrayList(u8)) !u32 {
    // unlike regular section, we don't emit the count
    const header_size = 1 + 5;
    const offset = @as(u32, @intCast(bytes.items.len));
    try bytes.appendSlice(&[_]u8{0} ** header_size);
    return offset;
}

fn writeVecSectionHeader(buffer: []u8, offset: u32, section: std.wasm.Section, size: u32, items: u32) !void {
    var buf: [1 + 5 + 5]u8 = undefined;
    buf[0] = @intFromEnum(section);
    leb.writeUnsignedFixed(5, buf[1..6], size);
    leb.writeUnsignedFixed(5, buf[6..], items);
    buffer[offset..][0..buf.len].* = buf;
}

fn writeCustomSectionHeader(buffer: []u8, offset: u32, size: u32) !void {
    var buf: [1 + 5]u8 = undefined;
    buf[0] = 0; // 0 = 'custom' section
    leb.writeUnsignedFixed(5, buf[1..6], size);
    buffer[offset..][0..buf.len].* = buf;
}

fn emitLinkSection(wasm: *Wasm, binary_bytes: *std.ArrayList(u8), symbol_table: *std.AutoArrayHashMap(SymbolLoc, u32)) !void {
    const offset = try reserveCustomSectionHeader(binary_bytes);
    const writer = binary_bytes.writer();
    // emit "linking" custom section name
    const section_name = "linking";
    try leb.writeULEB128(writer, section_name.len);
    try writer.writeAll(section_name);

    // meta data version, which is currently '2'
    try leb.writeULEB128(writer, @as(u32, 2));

    // For each subsection type (found in types.Subsection) we can emit a section.
    // Currently, we only support emitting segment info and the symbol table.
    try wasm.emitSymbolTable(binary_bytes, symbol_table);
    try wasm.emitSegmentInfo(binary_bytes);

    const size = @as(u32, @intCast(binary_bytes.items.len - offset - 6));
    try writeCustomSectionHeader(binary_bytes.items, offset, size);
}

fn emitSymbolTable(wasm: *Wasm, binary_bytes: *std.ArrayList(u8), symbol_table: *std.AutoArrayHashMap(SymbolLoc, u32)) !void {
    const writer = binary_bytes.writer();

    try leb.writeULEB128(writer, @intFromEnum(types.SubsectionType.WASM_SYMBOL_TABLE));
    const table_offset = binary_bytes.items.len;

    var symbol_count: u32 = 0;
    for (wasm.resolved_symbols.keys()) |sym_loc| {
        const symbol = sym_loc.getSymbol(wasm).*;
        if (symbol.tag == .dead) continue; // Do not emit dead symbols
        try symbol_table.putNoClobber(sym_loc, symbol_count);
        symbol_count += 1;
        log.debug("Emit symbol: {}", .{symbol});
        try leb.writeULEB128(writer, @intFromEnum(symbol.tag));
        try leb.writeULEB128(writer, symbol.flags);

        const sym_name = if (wasm.export_names.get(sym_loc)) |exp_name| wasm.string_table.get(exp_name) else sym_loc.getName(wasm);
        switch (symbol.tag) {
            .data => {
                try leb.writeULEB128(writer, @as(u32, @intCast(sym_name.len)));
                try writer.writeAll(sym_name);

                if (symbol.isDefined()) {
                    try leb.writeULEB128(writer, symbol.index);
                    const atom_index = wasm.symbol_atom.get(sym_loc).?;
                    const atom = wasm.getAtom(atom_index);
                    try leb.writeULEB128(writer, @as(u32, atom.offset));
                    try leb.writeULEB128(writer, @as(u32, atom.size));
                }
            },
            .section => {
                try leb.writeULEB128(writer, symbol.index);
            },
            else => {
                try leb.writeULEB128(writer, symbol.index);
                if (symbol.isDefined()) {
                    try leb.writeULEB128(writer, @as(u32, @intCast(sym_name.len)));
                    try writer.writeAll(sym_name);
                }
            },
        }
    }

    var buf: [10]u8 = undefined;
    leb.writeUnsignedFixed(5, buf[0..5], @as(u32, @intCast(binary_bytes.items.len - table_offset + 5)));
    leb.writeUnsignedFixed(5, buf[5..], symbol_count);
    try binary_bytes.insertSlice(table_offset, &buf);
}

fn emitSegmentInfo(wasm: *Wasm, binary_bytes: *std.ArrayList(u8)) !void {
    const writer = binary_bytes.writer();
    try leb.writeULEB128(writer, @intFromEnum(types.SubsectionType.WASM_SEGMENT_INFO));
    const segment_offset = binary_bytes.items.len;

    try leb.writeULEB128(writer, @as(u32, @intCast(wasm.segment_info.count())));
    for (wasm.segment_info.values()) |segment_info| {
        log.debug("Emit segment: {s} align({d}) flags({b})", .{
            segment_info.name,
            @ctz(segment_info.alignment),
            segment_info.flags,
        });
        try leb.writeULEB128(writer, @as(u32, @intCast(segment_info.name.len)));
        try writer.writeAll(segment_info.name);
        try leb.writeULEB128(writer, @ctz(segment_info.alignment));
        try leb.writeULEB128(writer, segment_info.flags);
    }

    var buf: [5]u8 = undefined;
    leb.writeUnsignedFixed(5, &buf, @as(u32, @intCast(binary_bytes.items.len - segment_offset)));
    try binary_bytes.insertSlice(segment_offset, &buf);
}

pub fn getULEB128Size(uint_value: anytype) u32 {
    const T = @TypeOf(uint_value);
    const U = if (@typeInfo(T).Int.bits < 8) u8 else T;
    var value = @as(U, @intCast(uint_value));

    var size: u32 = 0;
    while (value != 0) : (size += 1) {
        value >>= 7;
    }
    return size;
}

/// For each relocatable section, emits a custom "relocation.<section_name>" section
fn emitCodeRelocations(
    wasm: *Wasm,
    binary_bytes: *std.ArrayList(u8),
    section_index: u32,
    symbol_table: std.AutoArrayHashMap(SymbolLoc, u32),
) !void {
    const code_index = wasm.code_section_index orelse return;
    const writer = binary_bytes.writer();
    const header_offset = try reserveCustomSectionHeader(binary_bytes);

    // write custom section information
    const name = "reloc.CODE";
    try leb.writeULEB128(writer, @as(u32, @intCast(name.len)));
    try writer.writeAll(name);
    try leb.writeULEB128(writer, section_index);
    const reloc_start = binary_bytes.items.len;

    var count: u32 = 0;
    var atom: *Atom = wasm.getAtomPtr(wasm.atoms.get(code_index).?);
    // for each atom, we calculate the uleb size and append that
    var size_offset: u32 = 5; // account for code section size leb128
    while (true) {
        size_offset += getULEB128Size(atom.size);
        for (atom.relocs.items) |relocation| {
            count += 1;
            const sym_loc: SymbolLoc = .{ .file = atom.file, .index = relocation.index };
            const symbol_index = symbol_table.get(sym_loc).?;
            try leb.writeULEB128(writer, @intFromEnum(relocation.relocation_type));
            const offset = atom.offset + relocation.offset + size_offset;
            try leb.writeULEB128(writer, offset);
            try leb.writeULEB128(writer, symbol_index);
            if (relocation.relocation_type.addendIsPresent()) {
                try leb.writeILEB128(writer, relocation.addend);
            }
            log.debug("Emit relocation: {}", .{relocation});
        }
        atom = if (atom.prev) |prev| wasm.getAtomPtr(prev) else break;
    }
    if (count == 0) return;
    var buf: [5]u8 = undefined;
    leb.writeUnsignedFixed(5, &buf, count);
    try binary_bytes.insertSlice(reloc_start, &buf);
    const size = @as(u32, @intCast(binary_bytes.items.len - header_offset - 6));
    try writeCustomSectionHeader(binary_bytes.items, header_offset, size);
}

fn emitDataRelocations(
    wasm: *Wasm,
    binary_bytes: *std.ArrayList(u8),
    section_index: u32,
    symbol_table: std.AutoArrayHashMap(SymbolLoc, u32),
) !void {
    if (wasm.data_segments.count() == 0) return;
    const writer = binary_bytes.writer();
    const header_offset = try reserveCustomSectionHeader(binary_bytes);

    // write custom section information
    const name = "reloc.DATA";
    try leb.writeULEB128(writer, @as(u32, @intCast(name.len)));
    try writer.writeAll(name);
    try leb.writeULEB128(writer, section_index);
    const reloc_start = binary_bytes.items.len;

    var count: u32 = 0;
    // for each atom, we calculate the uleb size and append that
    var size_offset: u32 = 5; // account for code section size leb128
    for (wasm.data_segments.values()) |segment_index| {
        var atom: *Atom = wasm.getAtomPtr(wasm.atoms.get(segment_index).?);
        while (true) {
            size_offset += getULEB128Size(atom.size);
            for (atom.relocs.items) |relocation| {
                count += 1;
                const sym_loc: SymbolLoc = .{
                    .file = atom.file,
                    .index = relocation.index,
                };
                const symbol_index = symbol_table.get(sym_loc).?;
                try leb.writeULEB128(writer, @intFromEnum(relocation.relocation_type));
                const offset = atom.offset + relocation.offset + size_offset;
                try leb.writeULEB128(writer, offset);
                try leb.writeULEB128(writer, symbol_index);
                if (relocation.relocation_type.addendIsPresent()) {
                    try leb.writeILEB128(writer, relocation.addend);
                }
                log.debug("Emit relocation: {}", .{relocation});
            }
            atom = if (atom.prev) |prev| wasm.getAtomPtr(prev) else break;
        }
    }
    if (count == 0) return;

    var buf: [5]u8 = undefined;
    leb.writeUnsignedFixed(5, &buf, count);
    try binary_bytes.insertSlice(reloc_start, &buf);
    const size = @as(u32, @intCast(binary_bytes.items.len - header_offset - 6));
    try writeCustomSectionHeader(binary_bytes.items, header_offset, size);
}

fn hasPassiveInitializationSegments(wasm: *const Wasm) bool {
    var it = wasm.data_segments.iterator();
    while (it.next()) |entry| {
        const segment: Segment = wasm.segments.items[entry.value_ptr.*];
        if (segment.needsPassiveInitialization(wasm.base.options.import_memory, entry.key_ptr.*)) {
            return true;
        }
    }
    return false;
}

pub fn getTypeIndex(wasm: *const Wasm, func_type: std.wasm.Type) ?u32 {
    var index: u32 = 0;
    while (index < wasm.func_types.items.len) : (index += 1) {
        if (wasm.func_types.items[index].eql(func_type)) return index;
    }
    return null;
}

/// Searches for a matching function signature. When no matching signature is found,
/// a new entry will be made. The value returned is the index of the type within `wasm.func_types`.
pub fn putOrGetFuncType(wasm: *Wasm, func_type: std.wasm.Type) !u32 {
    if (wasm.getTypeIndex(func_type)) |index| {
        return index;
    }

    // functype does not exist.
    const index = @as(u32, @intCast(wasm.func_types.items.len));
    const params = try wasm.base.allocator.dupe(std.wasm.Valtype, func_type.params);
    errdefer wasm.base.allocator.free(params);
    const returns = try wasm.base.allocator.dupe(std.wasm.Valtype, func_type.returns);
    errdefer wasm.base.allocator.free(returns);
    try wasm.func_types.append(wasm.base.allocator, .{
        .params = params,
        .returns = returns,
    });
    return index;
}

/// For the given `decl_index`, stores the corresponding type representing the function signature.
/// Asserts declaration has an associated `Atom`.
/// Returns the index into the list of types.
pub fn storeDeclType(wasm: *Wasm, decl_index: Module.Decl.Index, func_type: std.wasm.Type) !u32 {
    const atom_index = wasm.decls.get(decl_index).?;
    const index = try wasm.putOrGetFuncType(func_type);
    try wasm.atom_types.put(wasm.base.allocator, atom_index, index);
    return index;
}
